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::{
10 DataServiceError, GraphNode, GraphOperation, ObjectLocation, RuntimeError, UserContext,
11};
12
13tokio::task_local! {
14 static GRAPH_FIX_TIME: teaql_core::time::Timestamp;
15 static GRAPH_FIX_EVIDENCE: Arc<std::sync::Mutex<Vec<crate::FixEvidence>>>;
16}
17
18pub(crate) fn current_graph_fix_time() -> teaql_core::time::Timestamp {
19 GRAPH_FIX_TIME
20 .try_with(|value| *value)
21 .unwrap_or_else(|_| teaql_core::time::Timestamp::now())
22}
23
24pub(crate) fn record_graph_fix_evidence(evidence: crate::FixEvidence) {
25 let _ = GRAPH_FIX_EVIDENCE.try_with(|current| current.lock().unwrap().push(evidence));
26}
27
28pub(crate) trait DynGraphSaver: Send + Sync {
38 fn save_graph_dyn<'a>(
39 &'a self,
40 context: &'a UserContext,
41 node: GraphNode,
42 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
43
44 fn save_ledger_dyn<'a>(
45 &'a self,
46 context: &'a UserContext,
47 node: GraphNode,
48 root: crate::EntityRuntimeState,
49 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
50}
51
52pub(crate) struct GraphSaverFor<E> {
58 _marker: PhantomData<fn() -> E>,
59}
60
61impl<E> GraphSaverFor<E> {
62 pub(crate) fn new() -> Self {
63 Self {
64 _marker: PhantomData,
65 }
66 }
67}
68
69impl<E> DynGraphSaver for GraphSaverFor<E>
70where
71 E: teaql_data_service::QueryExecutor
72 + teaql_data_service::MutationExecutor
73 + teaql_data_service::TransactionExecutor
74 + Send
75 + Sync
76 + 'static,
77 for<'tx> <E as teaql_data_service::TransactionExecutor>::Tx<'tx>: Send + Sync,
78{
79 fn save_graph_dyn<'a>(
80 &'a self,
81 context: &'a UserContext,
82 node: GraphNode,
83 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
84 Box::pin(async move {
85 let entity = node.entity.clone();
86 let executor = context
87 .require_resource::<E>()
88 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
89 let tx = teaql_data_service::TransactionExecutor::begin(&*executor)
90 .await
91 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
92 let result = {
93 let eds = crate::EntityDataService::for_executor(context, entity, &tx);
94 eds.save_graph_internal(node).await
95 };
96 match result {
97 Ok(saved) => {
98 teaql_data_service::Transaction::commit(tx)
99 .await
100 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
101 Ok(saved)
102 }
103 Err(error) => {
104 teaql_data_service::Transaction::rollback(tx)
105 .await
106 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
107 Err(match error {
108 DataServiceError::Runtime(r) => r,
109 other => RuntimeError::Graph(other.to_string()),
110 })
111 }
112 }
113 })
114 }
115
116 fn save_ledger_dyn<'a>(
117 &'a self,
118 context: &'a UserContext,
119 mut node: GraphNode,
120 root: crate::EntityRuntimeState,
121 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
122 Box::pin(async move {
123 let entity = node.entity.clone();
124 let executor = context
125 .require_resource::<E>()
126 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
127 let tx = teaql_data_service::TransactionExecutor::begin(&*executor)
128 .await
129 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
130 let descriptor = context.require_entity(&entity)?;
131 let id_prop = descriptor.id_property().ok_or_else(|| {
132 RuntimeError::Graph(format!("entity {entity} has no id property"))
133 })?;
134 let current_id = node
135 .values
136 .get(&id_prop.name)
137 .cloned()
138 .unwrap_or(Value::I64(0));
139 let root_key = crate::EntityKey::new(entity.clone(), current_id);
140 let was_new = root.new_keys().contains(&root_key);
141 let was_deleted = root.deleted_keys().contains(&root_key);
142 let original_version = root.get_original_version(&root_key);
143 let result = {
144 let eds = crate::EntityDataService::for_executor(context, &entity, &tx);
145 eds.execute_ledger_plan_internal(root.clone()).await
146 };
147 let generated_ids = match result {
148 Ok(ids) => {
149 teaql_data_service::Transaction::commit(tx)
150 .await
151 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
152 ids
153 }
154 Err(error) => {
155 teaql_data_service::Transaction::rollback(tx)
156 .await
157 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
158 return Err(match error {
159 DataServiceError::Runtime(r) => r,
160 other => RuntimeError::Graph(other.to_string()),
161 });
162 }
163 };
164
165 if let Some(new_id) = generated_ids.get(&root_key) {
166 node.values.insert(id_prop.name.clone(), new_id.clone());
167 }
168 if was_deleted {
169 if let Some(version_prop) = descriptor.version_property() {
170 if let Some(version) = saved_version(was_new, true, original_version) {
171 node.values
172 .insert(version_prop.name.clone(), Value::I64(version));
173 }
174 }
175 } else {
176 let persisted_id = node.values.get(&id_prop.name).cloned().ok_or_else(|| {
183 RuntimeError::Graph(format!(
184 "saved {entity} missing identity field {}",
185 id_prop.name
186 ))
187 })?;
188 let eds = crate::EntityDataService::for_executor(context, &entity, &*executor);
189 node.values = eds
190 .fetch_graph_current_row_internal(
191 &entity,
192 &id_prop.name,
193 &persisted_id,
194 Vec::new(),
195 )
196 .await
197 .map_err(|error| RuntimeError::Graph(error.to_string()))?
198 .map(Into::into)
199 .ok_or_else(|| {
200 RuntimeError::Graph(format!(
201 "persisted {entity} record could not be read back"
202 ))
203 })?;
204 }
205 root.clear_committed();
206 Ok(node)
207 })
208 }
209}
210
211fn saved_version(was_new: bool, was_deleted: bool, original_version: Option<i64>) -> Option<i64> {
212 if was_new {
213 Some(1)
214 } else if was_deleted {
215 original_version.map(|version| -(version.abs() + 1))
216 } else {
217 original_version.map(|version| version + 1)
218 }
219}
220
221#[cfg(test)]
222mod saved_version_tests {
223 use super::saved_version;
224
225 #[test]
226 fn create_returns_initial_version() {
227 assert_eq!(saved_version(true, false, None), Some(1));
228 }
229
230 #[test]
231 fn update_returns_incremented_version() {
232 assert_eq!(saved_version(false, false, Some(7)), Some(8));
233 }
234
235 #[test]
236 fn delete_returns_next_negative_version() {
237 assert_eq!(saved_version(false, true, Some(7)), Some(-8));
238 }
239}
240
241pub fn graph_node_from_entity<T: Entity>(
251 context: &UserContext,
252 entity: T,
253) -> Result<GraphNode, RuntimeError> {
254 let descriptor = T::entity_descriptor();
255 let loaded_fields = descriptor
256 .properties
257 .iter()
258 .filter(|property| entity.is_field_loaded(&property.name))
259 .map(|property| Value::Text(property.name.clone()))
260 .collect::<Vec<_>>();
261 let dirty_fields = entity.dirty_fields();
262 let original_values = entity.original_values();
263 let is_new = entity.is_new();
264 let is_deleted = entity.is_marked_as_delete();
265 let comment = entity.get_comment();
266 let mut node = graph_node_from_values(context, &descriptor.name, entity.into_values())?;
267 node.values
268 .insert("_loaded_fields".to_owned(), Value::List(loaded_fields));
269 node.dirty_fields = dirty_fields;
270 node.original_values = original_values.map(Into::into);
271 if is_new {
272 node.operation = GraphOperation::Create;
273 }
274 if is_deleted {
275 node.operation = GraphOperation::Remove;
276 node.relations.clear();
277 }
278 if let Some(c) = comment {
279 node.set_comment(c);
280 }
281 Ok(node)
282}
283
284fn graph_node_from_values(
288 context: &UserContext,
289 entity: &str,
290 values: MutationValues,
291) -> Result<GraphNode, RuntimeError> {
292 let descriptor = context.require_entity(entity)?;
293 let mut node = GraphNode::new(entity);
294
295 for (field, value) in values {
296 if field == "_comment" {
297 if let Value::Text(comment) = value {
298 node.set_comment(comment);
299 }
300 continue;
301 }
302 if field == "_dirty_fields" {
303 if let Value::List(fields) = value {
304 let mut dirty = BTreeSet::new();
305 for f in fields {
306 if let Value::Text(t) = f {
307 dirty.insert(t);
308 }
309 }
310 node.dirty_fields = Some(dirty);
311 }
312 continue;
313 }
314 if field == "_original_values" {
315 if let Value::Object(orig) = value {
316 node.original_values = Some(orig.into());
317 }
318 continue;
319 }
320 if field == "_is_new" {
321 if matches!(value, Value::Bool(true)) {
322 node.operation = GraphOperation::Create;
323 }
324 continue;
325 }
326 if field == "_is_deleted" {
327 if matches!(value, Value::Bool(true)) {
328 node.operation = GraphOperation::Remove;
329 }
330 continue;
331 }
332 let Some(relation) = descriptor.relation_by_name(&field) else {
333 node.values.insert(field, value);
334 continue;
335 };
336
337 match value {
338 Value::Null => {
339 node.relations.entry(field).or_default();
340 }
341 Value::Object(record) => {
342 let child =
343 graph_node_from_values(context, &relation.target_entity, record.into())?;
344 node.relations.entry(field).or_default().push(child);
345 }
346 Value::List(values) => {
347 let children = node.relations.entry(field.clone()).or_default();
348 for value in values {
349 let Value::Object(record) = value else {
350 return Err(RuntimeError::Graph(format!(
351 "relation {}.{} expects object children, got {:?}",
352 entity, field, value
353 )));
354 };
355 children.push(graph_node_from_values(
356 context,
357 &relation.target_entity,
358 record.into(),
359 )?);
360 }
361 }
362 other => {
363 return Err(RuntimeError::Graph(format!(
364 "relation {}.{} expects object/list/null, got {:?}",
365 entity, field, other
366 )));
367 }
368 }
369 }
370
371 Ok(node)
372}
373
374fn merge_relation_mutations_into_root(
375 root: &crate::EntityRuntimeState,
376 node: &GraphNode,
377) -> Result<(), RuntimeError> {
378 for children in node.relations.values() {
379 for child in children {
380 let id = child.values.get("id").cloned().ok_or_else(|| {
381 RuntimeError::Graph(format!(
382 "related mutation {} is missing its id",
383 child.entity
384 ))
385 })?;
386 let key = crate::EntityKey::new(child.entity.clone(), id);
387
388 match child.operation {
389 GraphOperation::Create => {
390 root.mark_as_new(key.clone());
391 for (field, value) in &child.values {
392 root.set(key.clone(), field, value.clone());
393 }
394 }
395 GraphOperation::Upsert => {
396 if let Some(fields) = &child.dirty_fields {
397 for field in fields {
398 if let Some(value) = child.values.get(field) {
399 root.set(key.clone(), field, value.clone());
400 }
401 }
402 }
403 }
404 GraphOperation::Remove => root.mark_as_delete(key.clone()),
405 GraphOperation::Reference => {}
406 }
407
408 if let Some(version) = child
409 .original_values
410 .as_ref()
411 .and_then(|values| values.get("version"))
412 .and_then(Value::try_i64)
413 {
414 root.set_original_version(key, version);
415 }
416 merge_relation_mutations_into_root(root, child)?;
417 }
418 }
419 Ok(())
420}
421
422fn hydrate_ledger_relations(
423 context: &UserContext,
424 root: &crate::EntityRuntimeState,
425 node: &mut GraphNode,
426 visited: &mut BTreeSet<crate::EntityKey>,
427) -> Result<(), RuntimeError> {
428 let descriptor = context.require_entity(&node.entity)?;
429 for relation in &descriptor.relations {
430 let Some(local_value) = node.values.get(&relation.local_key).cloned() else {
431 continue;
432 };
433 let existing = node.relations.entry(relation.name.clone()).or_default();
434 let existing_keys = existing
435 .iter()
436 .filter_map(|child| {
437 child
438 .values
439 .get("id")
440 .cloned()
441 .map(|id| crate::EntityKey::new(child.entity.clone(), id))
442 })
443 .collect::<BTreeSet<_>>();
444 let mut discovered = Vec::new();
445 for (key, changes) in root.current_change_set().changes() {
446 if key.entity.as_ref() != relation.target_entity || existing_keys.contains(key) {
447 continue;
448 }
449 let foreign_value = if relation.foreign_key == "id" {
450 Some(&key.id)
451 } else {
452 changes.get(&relation.foreign_key)
453 };
454 if foreign_value != Some(&local_value) || !visited.insert(key.clone()) {
455 continue;
456 }
457 let mut values: crate::EntityValues = changes.clone().into();
458 values
459 .entry("id".to_owned())
460 .or_insert_with(|| key.id.clone());
461 let operation = if root.deleted_keys().contains(key) {
462 GraphOperation::Remove
463 } else if root.new_keys().contains(key) || root.get_original_version(key).is_none() {
464 GraphOperation::Create
465 } else {
466 GraphOperation::Upsert
467 };
468 let mut child = GraphNode::new(key.entity.to_string());
469 child.values = values;
470 child.operation = operation;
471 hydrate_ledger_relations(context, root, &mut child, visited)?;
472 discovered.push(child);
473 }
474 existing.extend(discovered);
475 }
476 Ok(())
477}
478
479fn preflight_graph(
480 context: &UserContext,
481 node: &mut GraphNode,
482 location: &ObjectLocation,
483 root: Option<&crate::EntityRuntimeState>,
484) -> Result<(), RuntimeError> {
485 if !matches!(
486 node.operation,
487 GraphOperation::Remove | GraphOperation::Reference
488 ) {
489 let before = node.values.clone();
490 let status = match node.operation {
491 GraphOperation::Create => crate::CheckObjectStatus::Create,
492 GraphOperation::Upsert => crate::CheckObjectStatus::Update,
493 GraphOperation::Remove | GraphOperation::Reference => unreachable!(),
494 };
495 crate::mark_entity_status(&mut node.values, status);
496 let result = context.check_and_fix_values_at(&node.entity, &mut node.values, location);
497 crate::clear_entity_status(&mut node.values);
498 result?;
499
500 if let Some(root) = root {
501 if let Some(id) = node.values.get("id").cloned() {
502 let key = crate::EntityKey::new(node.entity.clone(), id);
503 for (field, value) in &node.values {
504 if before.get(field) != Some(value) {
505 root.set(key.clone(), field.clone(), value.clone());
506 }
507 }
508 }
509 }
510 }
511
512 for (relation, children) in &mut node.relations {
513 for (index, child) in children.iter_mut().enumerate() {
514 let child_location = location.clone().member(relation).element(index);
515 preflight_graph(context, child, &child_location, root)?;
516 }
517 }
518 Ok(())
519}
520
521pub trait AuditedSaveExt {
534 type Entity;
535
536 fn save<'a>(
537 self,
538 context: &'a UserContext,
539 ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>>;
540}
541
542impl<T> AuditedSaveExt for teaql_core::Audited<T>
543where
544 T: Entity + Send + 'static,
545{
546 type Entity = T;
547
548 fn save<'a>(
549 self,
550 context: &'a UserContext,
551 ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>> {
552 Box::pin(async move {
553 let entity_name = T::entity_descriptor().name;
554 let entity = self.into_entity(); let mut node = graph_node_from_entity(context, entity)?;
556 preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
557 let saver = context
558 .require_resource::<Arc<dyn DynGraphSaver>>()
559 .map_err(|e| {
560 RuntimeError::Graph(format!(
561 "no DynGraphSaver registered — did you call register_executor()? ({})",
562 e
563 ))
564 })?;
565 let saved = saver.save_graph_dyn(context, node).await?;
566 T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
567 .map_err(|e| RuntimeError::Graph(e.to_string()))
568 })
569 }
570}
571
572#[doc(hidden)]
579pub async fn save_audited_ledger_entity<T>(
580 audited: teaql_core::Audited<T>,
581 context: &UserContext,
582) -> Result<T, RuntimeError>
583where
584 T: crate::LedgerEntity + Send + 'static,
585{
586 let evidence = Arc::new(std::sync::Mutex::new(Vec::new()));
587 let result = GRAPH_FIX_TIME
588 .scope(
589 teaql_core::time::Timestamp::now(),
590 GRAPH_FIX_EVIDENCE.scope(
591 evidence.clone(),
592 save_audited_ledger_entity_inner(audited, context),
593 ),
594 )
595 .await;
596 context.replace_last_fix_evidence(evidence.lock().unwrap().clone());
597 result
598}
599
600#[doc(hidden)]
607pub async fn save_audited_ledger_entity_with_executor<T, E>(
608 audited: teaql_core::Audited<T>,
609 context: &UserContext,
610 executor: &E,
611) -> Result<(T, Option<crate::EntityRuntimeState>), RuntimeError>
612where
613 T: crate::LedgerEntity + Send + 'static,
614 E: teaql_data_service::QueryExecutor + teaql_data_service::MutationExecutor + Send + Sync,
615{
616 let evidence = Arc::new(std::sync::Mutex::new(Vec::new()));
617 let result = GRAPH_FIX_TIME
618 .scope(
619 teaql_core::time::Timestamp::now(),
620 GRAPH_FIX_EVIDENCE.scope(
621 evidence.clone(),
622 save_audited_ledger_entity_with_executor_inner(audited, context, executor),
623 ),
624 )
625 .await;
626 context.replace_last_fix_evidence(evidence.lock().unwrap().clone());
627 result
628}
629
630async fn save_audited_ledger_entity_with_executor_inner<T, E>(
631 audited: teaql_core::Audited<T>,
632 context: &UserContext,
633 executor: &E,
634) -> Result<(T, Option<crate::EntityRuntimeState>), RuntimeError>
635where
636 T: crate::LedgerEntity + Send + 'static,
637 E: teaql_data_service::QueryExecutor + teaql_data_service::MutationExecutor + Send + Sync,
638{
639 let entity = audited.into_entity();
640 let root = entity.entity_runtime_state();
641 let mut node = graph_node_from_entity(context, entity)?;
642
643 if let Some(root) = root {
644 let root_id = node.values.get("id").cloned().unwrap_or(Value::I64(0));
645 let root_key = crate::EntityKey::new(node.entity.clone(), root_id);
646 if let Some(changes) = root.current_change_set().changes().get(&root_key) {
647 for (field, value) in changes {
648 node.values.insert(field.clone(), value.clone());
649 }
650 }
651 let mut visited = BTreeSet::from([root_key.clone()]);
652 hydrate_ledger_relations(context, &root, &mut node, &mut visited)?;
653 preflight_graph(context, &mut node, &ObjectLocation::root(), Some(&root))?;
654 merge_relation_mutations_into_root(&root, &node)?;
655 let has_ledger_changes = !root.current_change_set().changes().is_empty()
656 || !root.deleted_keys().is_empty()
657 || !root.new_keys().is_empty();
658 if has_ledger_changes {
659 let entity_name = node.entity.clone();
660 let descriptor = context.require_entity(&entity_name)?;
661 let id_property = descriptor.id_property().ok_or_else(|| {
662 RuntimeError::Graph(format!("entity {entity_name} has no id property"))
663 })?;
664 let was_new = root.new_keys().contains(&root_key);
665 let was_deleted = root.deleted_keys().contains(&root_key);
666 let original_version = root.get_original_version(&root_key);
667 let data_service =
668 crate::EntityDataService::for_executor(context, &entity_name, executor);
669 let generated_ids = data_service
670 .execute_ledger_plan_internal(root.clone())
671 .await
672 .map_err(data_service_error_into_runtime)?;
673
674 if let Some(new_id) = generated_ids.get(&root_key) {
675 node.values.insert(id_property.name.clone(), new_id.clone());
676 }
677 if was_deleted {
678 if let Some(version_property) = descriptor.version_property() {
679 if let Some(version) = saved_version(was_new, true, original_version) {
680 node.values
681 .insert(version_property.name.clone(), Value::I64(version));
682 }
683 }
684 } else {
685 let persisted_id =
686 node.values.get(&id_property.name).cloned().ok_or_else(|| {
687 RuntimeError::Graph(format!(
688 "saved {entity_name} missing identity field {}",
689 id_property.name
690 ))
691 })?;
692 node.values = data_service
693 .fetch_graph_current_row_internal(
694 &entity_name,
695 &id_property.name,
696 &persisted_id,
697 Vec::new(),
698 )
699 .await
700 .map_err(data_service_error_into_runtime)?
701 .map(Into::into)
702 .ok_or_else(|| {
703 RuntimeError::Graph(format!(
704 "persisted {entity_name} record could not be read back"
705 ))
706 })?;
707 }
708 let entity = T::from_compact_row(teaql_core::CompactRow::from_map(node.values.into()))
709 .map_err(|error| RuntimeError::Graph(error.to_string()))?;
710 return Ok((entity, Some(root)));
711 }
712 }
713
714 preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
715 let entity_name = node.entity.clone();
716 let saved = crate::EntityDataService::for_executor(context, entity_name, executor)
717 .save_graph_internal(node)
718 .await
719 .map_err(data_service_error_into_runtime)?;
720 let entity = T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
721 .map_err(|error| RuntimeError::Graph(error.to_string()))?;
722 Ok((entity, None))
723}
724
725fn data_service_error_into_runtime<E: std::error::Error>(
726 error: DataServiceError<E>,
727) -> RuntimeError {
728 match error {
729 DataServiceError::Runtime(error) => error,
730 other => RuntimeError::Graph(other.to_string()),
731 }
732}
733
734async fn save_audited_ledger_entity_inner<T>(
735 audited: teaql_core::Audited<T>,
736 context: &UserContext,
737) -> Result<T, RuntimeError>
738where
739 T: crate::LedgerEntity + Send + 'static,
740{
741 let entity_name = T::entity_descriptor().name;
742 let entity = audited.into_entity();
743 let root = entity.entity_runtime_state();
744 let mut node = graph_node_from_entity(context, entity)?;
745 let saver = context
746 .require_resource::<Arc<dyn DynGraphSaver>>()
747 .map_err(|e| {
748 RuntimeError::Graph(format!(
749 "no DynGraphSaver registered — did you call register_executor()? ({e})"
750 ))
751 })?;
752
753 if let Some(root) = root {
754 let root_id = node.values.get("id").cloned().unwrap_or(Value::I64(0));
755 let root_key = crate::EntityKey::new(node.entity.clone(), root_id);
756 if let Some(changes) = root.current_change_set().changes().get(&root_key) {
757 for (field, value) in changes {
758 node.values.insert(field.clone(), value.clone());
759 }
760 }
761 let mut visited = BTreeSet::from([root_key]);
762 hydrate_ledger_relations(context, &root, &mut node, &mut visited)?;
763 preflight_graph(context, &mut node, &ObjectLocation::root(), Some(&root))?;
764 merge_relation_mutations_into_root(&root, &node)?;
765 let has_ledger_changes = !root.current_change_set().changes().is_empty()
766 || !root.deleted_keys().is_empty()
767 || !root.new_keys().is_empty();
768 if has_ledger_changes {
769 let saved = saver.save_ledger_dyn(context, node, root).await?;
770 return T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
771 .map_err(|e| RuntimeError::Graph(e.to_string()));
772 }
773 }
774
775 preflight_graph(context, &mut node, &ObjectLocation::root(), None)?;
776 let saved = saver.save_graph_dyn(context, node).await?;
777 T::from_compact_row(teaql_core::CompactRow::from_map(saved.values.into()))
778 .map_err(|e| RuntimeError::Graph(e.to_string()))
779}