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