teaql_runtime/
entity_save.rs1use std::collections::BTreeSet;
2use std::future::Future;
3use std::marker::PhantomData;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use teaql_core::{Entity, Record, 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::EntityRoot,
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::EntityRoot,
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 generated_ids = eds
89 .execute_ledger_plan_internal(root.clone())
90 .await
91 .map_err(|e| match e {
92 DataServiceError::Runtime(r) => r,
93 other => RuntimeError::Graph(other.to_string()),
94 })?;
95
96 let descriptor = context.require_entity(&entity).unwrap();
97 if let Some(id_prop) = descriptor.id_property() {
98 let current_id = node
99 .values
100 .get(&id_prop.name)
101 .cloned()
102 .unwrap_or(Value::I64(0));
103 let root_key = crate::EntityKey::new(entity.clone(), current_id);
104 if let Some(new_id) = generated_ids.get(&root_key) {
105 node.values.insert(id_prop.name.clone(), new_id.clone());
106 }
107 if let Some(changes) = root.current_change_set().changes().get(&root_key) {
108 for (field, value) in changes {
109 node.values.insert(field.clone(), value.clone());
110 }
111 }
112 }
113 Ok(node)
114 })
115 }
116}
117
118pub fn graph_node_from_entity<T: Entity>(
128 context: &UserContext,
129 entity: T,
130) -> Result<GraphNode, RuntimeError> {
131 let descriptor = T::entity_descriptor();
132 let dirty_fields = entity.dirty_fields();
133 let original_values = entity.original_values();
134 let is_deleted = entity.is_marked_as_delete();
135 let comment = entity.get_comment();
136 let mut node = graph_node_from_record(context, &descriptor.name, entity.into_record())?;
137 node.dirty_fields = dirty_fields;
138 node.original_values = original_values;
139 if is_deleted {
140 node.operation = GraphOperation::Remove;
141 node.relations.clear();
142 }
143 if let Some(c) = comment {
144 node.set_comment(c);
145 }
146 Ok(node)
147}
148
149fn graph_node_from_record(
153 context: &UserContext,
154 entity: &str,
155 record: Record,
156) -> Result<GraphNode, RuntimeError> {
157 let descriptor = context.require_entity(entity)?;
158 let mut node = GraphNode::new(entity);
159
160 for (field, value) in record {
161 if field == "_comment" {
162 if let Value::Text(comment) = value {
163 node.set_comment(comment);
164 }
165 continue;
166 }
167 if field == "_dirty_fields" {
168 if let Value::List(fields) = value {
169 let mut dirty = BTreeSet::new();
170 for f in fields {
171 if let Value::Text(t) = f {
172 dirty.insert(t);
173 }
174 }
175 node.dirty_fields = Some(dirty);
176 }
177 continue;
178 }
179 if field == "_original_values" {
180 if let Value::Object(orig) = value {
181 node.original_values = Some(orig);
182 }
183 continue;
184 }
185 let Some(relation) = descriptor.relation_by_name(&field) else {
186 node.values.insert(field, value);
187 continue;
188 };
189
190 match value {
191 Value::Null => {
192 node.relations.entry(field).or_default();
193 }
194 Value::Object(record) => {
195 let child = graph_node_from_record(context, &relation.target_entity, record)?;
196 node.relations.entry(field).or_default().push(child);
197 }
198 Value::List(values) => {
199 let children = node.relations.entry(field.clone()).or_default();
200 for value in values {
201 let Value::Object(record) = value else {
202 return Err(RuntimeError::Graph(format!(
203 "relation {}.{} expects object children, got {:?}",
204 entity, field, value
205 )));
206 };
207 children.push(graph_node_from_record(
208 context,
209 &relation.target_entity,
210 record,
211 )?);
212 }
213 }
214 other => {
215 return Err(RuntimeError::Graph(format!(
216 "relation {}.{} expects object/list/null, got {:?}",
217 entity, field, other
218 )));
219 }
220 }
221 }
222
223 Ok(node)
224}
225
226pub trait AuditedSaveExt {
239 type Entity;
240
241 fn save<'a>(
242 self,
243 context: &'a UserContext,
244 ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>>;
245}
246
247impl<T> AuditedSaveExt for teaql_core::Audited<T>
248where
249 T: Entity + Send + 'static,
250{
251 type Entity = T;
252
253 fn save<'a>(
254 self,
255 context: &'a UserContext,
256 ) -> Pin<Box<dyn Future<Output = Result<Self::Entity, RuntimeError>> + Send + 'a>> {
257 Box::pin(async move {
258 let entity_name = T::entity_descriptor().name;
259 let entity = self.into_entity(); let node = graph_node_from_entity(context, entity)?;
261 let saver = context
262 .require_resource::<Arc<dyn DynGraphSaver>>()
263 .map_err(|e| {
264 RuntimeError::Graph(format!(
265 "no DynGraphSaver registered — did you call register_executor()? ({})",
266 e
267 ))
268 })?;
269 let saved = saver.save_graph_dyn(context, node).await?;
270 T::from_record(saved.values).map_err(|e| RuntimeError::Graph(e.to_string()))
271 })
272 }
273}
274
275#[doc(hidden)]
282pub async fn save_audited_ledger_entity<T>(
283 audited: teaql_core::Audited<T>,
284 context: &UserContext,
285) -> Result<T, RuntimeError>
286where
287 T: crate::LedgerEntity + Send + 'static,
288{
289 let entity_name = T::entity_descriptor().name;
290 let entity = audited.into_entity();
291 let root = entity.entity_root();
292 let node = graph_node_from_entity(context, entity)?;
293 let saver = context
294 .require_resource::<Arc<dyn DynGraphSaver>>()
295 .map_err(|e| {
296 RuntimeError::Graph(format!(
297 "no DynGraphSaver registered — did you call register_executor()? ({e})"
298 ))
299 })?;
300
301 if let Some(root) = root {
302 let has_ledger_changes = !root.current_change_set().changes().is_empty()
303 || !root.deleted_keys().is_empty()
304 || !root.new_keys().is_empty();
305 if has_ledger_changes {
306 let saved = saver.save_ledger_dyn(context, node, root).await?;
307 return T::from_record(saved.values).map_err(|e| RuntimeError::Graph(e.to_string()));
308 }
309 }
310
311 let saved = saver.save_graph_dyn(context, node).await?;
312 T::from_record(saved.values).map_err(|e| RuntimeError::Graph(e.to_string()))
313}