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 trait DynGraphSaver: Send + Sync {
21 fn save_graph_dyn<'a>(
22 &'a self,
23 ctx: &'a UserContext,
24 entity: &'a str,
25 node: GraphNode,
26 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
27}
28
29pub struct GraphSaverFor<E> {
35 _marker: PhantomData<fn() -> E>,
36}
37
38impl<E> GraphSaverFor<E> {
39 pub fn new() -> Self {
40 Self {
41 _marker: PhantomData,
42 }
43 }
44}
45
46impl<E> DynGraphSaver for GraphSaverFor<E>
47where
48 E: teaql_data_service::QueryExecutor
49 + teaql_data_service::MutationExecutor
50 + Send
51 + Sync
52 + 'static,
53{
54 fn save_graph_dyn<'a>(
55 &'a self,
56 ctx: &'a UserContext,
57 entity: &'a str,
58 node: GraphNode,
59 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
60 Box::pin(async move {
61 let eds = ctx
62 .entity_data_service::<E>(entity)
63 .map_err(|e| RuntimeError::Graph(e.to_string()))?;
64 eds.save_graph(node).await.map_err(|e| match e {
65 DataServiceError::Runtime(r) => r,
66 other => RuntimeError::Graph(other.to_string()),
67 })
68 })
69 }
70}
71
72pub fn graph_node_from_entity<T: Entity>(
82 ctx: &UserContext,
83 entity: T,
84) -> Result<GraphNode, RuntimeError> {
85 let descriptor = T::entity_descriptor();
86 let dirty_fields = entity.dirty_fields();
87 let original_values = entity.original_values();
88 let is_deleted = entity.is_marked_as_delete();
89 let comment = entity.get_comment();
90 let mut node = graph_node_from_record(ctx, &descriptor.name, entity.into_record())?;
91 node.dirty_fields = dirty_fields;
92 node.original_values = original_values;
93 if is_deleted {
94 node.operation = GraphOperation::Remove;
95 node.relations.clear();
96 }
97 if let Some(c) = comment {
98 node.set_comment(c);
99 }
100 Ok(node)
101}
102
103fn graph_node_from_record(
107 ctx: &UserContext,
108 entity: &str,
109 record: Record,
110) -> Result<GraphNode, RuntimeError> {
111 let descriptor = ctx.require_entity(entity)?;
112 let mut node = GraphNode::new(entity);
113
114 for (field, value) in record {
115 if field == "_comment" {
116 if let Value::Text(comment) = value {
117 node.set_comment(comment);
118 }
119 continue;
120 }
121 if field == "_dirty_fields" {
122 if let Value::List(fields) = value {
123 let mut dirty = BTreeSet::new();
124 for f in fields {
125 if let Value::Text(t) = f {
126 dirty.insert(t);
127 }
128 }
129 node.dirty_fields = Some(dirty);
130 }
131 continue;
132 }
133 if field == "_original_values" {
134 if let Value::Object(orig) = value {
135 node.original_values = Some(orig);
136 }
137 continue;
138 }
139 let Some(relation) = descriptor.relation_by_name(&field) else {
140 node.values.insert(field, value);
141 continue;
142 };
143
144 match value {
145 Value::Null => {
146 node.relations.entry(field).or_default();
147 }
148 Value::Object(record) => {
149 let child = graph_node_from_record(ctx, &relation.target_entity, record)?;
150 node.relations.entry(field).or_default().push(child);
151 }
152 Value::List(values) => {
153 let children = node.relations.entry(field.clone()).or_default();
154 for value in values {
155 let Value::Object(record) = value else {
156 return Err(RuntimeError::Graph(format!(
157 "relation {}.{} expects object children, got {:?}",
158 entity, field, value
159 )));
160 };
161 children.push(graph_node_from_record(
162 ctx,
163 &relation.target_entity,
164 record,
165 )?);
166 }
167 }
168 other => {
169 return Err(RuntimeError::Graph(format!(
170 "relation {}.{} expects object/list/null, got {:?}",
171 entity, field, other
172 )));
173 }
174 }
175 }
176
177 Ok(node)
178}
179
180pub trait AuditedSaveExt {
193 fn save<'a>(
194 self,
195 ctx: &'a UserContext,
196 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
197}
198
199impl<T> AuditedSaveExt for teaql_core::Audited<T>
200where
201 T: Entity + Send + 'static,
202{
203 fn save<'a>(
204 self,
205 ctx: &'a UserContext,
206 ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
207 Box::pin(async move {
208 let entity_name = T::entity_descriptor().name;
209 let entity = self.into_entity(); let node = graph_node_from_entity(ctx, entity)?;
211 let saver = ctx
212 .require_resource::<Arc<dyn DynGraphSaver>>()
213 .map_err(|e| {
214 RuntimeError::Graph(format!(
215 "no DynGraphSaver registered — did you call register_executor()? ({})",
216 e
217 ))
218 })?;
219 saver.save_graph_dyn(ctx, &entity_name, node).await
220 })
221 }
222}