Skip to main content

teaql_runtime/
entity_save.rs

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, Record, Value};
8
9use crate::{DataServiceError, GraphNode, GraphOperation, RuntimeError, UserContext};
10
11// ---------------------------------------------------------------------------
12// DynGraphSaver — type-erased graph save capability
13// ---------------------------------------------------------------------------
14
15/// Object-safe trait for saving a [`GraphNode`] tree to the database.
16///
17/// A concrete implementation is registered in [`UserContext`] during setup so
18/// that [`Audited::save`] can persist entities without exposing the underlying
19/// executor type to business code.
20pub(crate) trait DynGraphSaver: Send + Sync {
21    fn save_graph_dyn<'a>(
22        &'a self,
23        ctx: &'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        ctx: &'a UserContext,
30        node: GraphNode,
31        root: crate::EntityRoot,
32    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
33}
34
35/// Marker struct that implements [`DynGraphSaver`] for a specific executor type `E`.
36///
37/// `E` is the full executor type (e.g. `SqlDataServiceExecutor<SqliteDialect, …>`).
38/// The struct itself is zero-sized; the actual executor is retrieved from
39/// [`UserContext`] at call time.
40pub(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        ctx: &'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 = ctx
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        ctx: &'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 = ctx
86                .entity_data_service::<E>(&entity)
87                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
88            let generated_ids = eds.execute_ledger_plan_internal(root)
89                .await
90                .map_err(|e| match e {
91                    DataServiceError::Runtime(r) => r,
92                    other => RuntimeError::Graph(other.to_string()),
93                })?;
94                
95            let descriptor = ctx.require_entity(&entity).unwrap();
96            if let Some(id_prop) = descriptor.id_property() {
97                let current_id = node.values.get(&id_prop.name).cloned().unwrap_or(Value::I64(0));
98                let root_key = crate::EntityKey::new(entity.clone(), current_id);
99                if let Some(new_id) = generated_ids.get(&root_key) {
100                    node.values.insert(id_prop.name.clone(), new_id.clone());
101                }
102            }
103            Ok(node)
104        })
105    }
106}
107
108// ---------------------------------------------------------------------------
109// Standalone graph-node extraction (no executor needed)
110// ---------------------------------------------------------------------------
111
112/// Convert a typed entity into a [`GraphNode`] tree.
113///
114/// This only requires metadata (entity descriptors) from the [`UserContext`],
115/// **not** the database executor.  It is the standalone equivalent of
116/// [`EntityDataService::graph_node_from_entity`].
117pub fn graph_node_from_entity<T: Entity>(
118    ctx: &UserContext,
119    entity: T,
120) -> Result<GraphNode, RuntimeError> {
121    let descriptor = T::entity_descriptor();
122    let dirty_fields = entity.dirty_fields();
123    let original_values = entity.original_values();
124    let is_deleted = entity.is_marked_as_delete();
125    let comment = entity.get_comment();
126    let mut node = graph_node_from_record(ctx, &descriptor.name, entity.into_record())?;
127    node.dirty_fields = dirty_fields;
128    node.original_values = original_values;
129    if is_deleted {
130        node.operation = GraphOperation::Remove;
131        node.relations.clear();
132    }
133    if let Some(c) = comment {
134        node.set_comment(c);
135    }
136    Ok(node)
137}
138
139/// Recursively convert a [`Record`] into a [`GraphNode`] tree.
140///
141/// Relations are resolved via the entity descriptors stored in `ctx`.
142fn graph_node_from_record(
143    ctx: &UserContext,
144    entity: &str,
145    record: Record,
146) -> Result<GraphNode, RuntimeError> {
147    let descriptor = ctx.require_entity(entity)?;
148    let mut node = GraphNode::new(entity);
149
150    for (field, value) in record {
151        if field == "_comment" {
152            if let Value::Text(comment) = value {
153                node.set_comment(comment);
154            }
155            continue;
156        }
157        if field == "_dirty_fields" {
158            if let Value::List(fields) = value {
159                let mut dirty = BTreeSet::new();
160                for f in fields {
161                    if let Value::Text(t) = f {
162                        dirty.insert(t);
163                    }
164                }
165                node.dirty_fields = Some(dirty);
166            }
167            continue;
168        }
169        if field == "_original_values" {
170            if let Value::Object(orig) = value {
171                node.original_values = Some(orig);
172            }
173            continue;
174        }
175        let Some(relation) = descriptor.relation_by_name(&field) else {
176            node.values.insert(field, value);
177            continue;
178        };
179
180        match value {
181            Value::Null => {
182                node.relations.entry(field).or_default();
183            }
184            Value::Object(record) => {
185                let child = graph_node_from_record(ctx, &relation.target_entity, record)?;
186                node.relations.entry(field).or_default().push(child);
187            }
188            Value::List(values) => {
189                let children = node.relations.entry(field.clone()).or_default();
190                for value in values {
191                    let Value::Object(record) = value else {
192                        return Err(RuntimeError::Graph(format!(
193                            "relation {}.{} expects object children, got {:?}",
194                            entity, field, value
195                        )));
196                    };
197                    children.push(graph_node_from_record(
198                        ctx,
199                        &relation.target_entity,
200                        record,
201                    )?);
202                }
203            }
204            other => {
205                return Err(RuntimeError::Graph(format!(
206                    "relation {}.{} expects object/list/null, got {:?}",
207                    entity, field, other
208                )));
209            }
210        }
211    }
212
213    Ok(node)
214}
215
216// ---------------------------------------------------------------------------
217// AuditedSaveExt — the `.save(&ctx)` method on `Audited<T>`
218// ---------------------------------------------------------------------------
219
220/// Extension trait that provides the `.save(&ctx)` method on [`Audited<T>`](teaql_core::Audited).
221///
222/// # Example
223/// ```ignore
224/// use teaql_runtime::AuditedSaveExt;
225///
226/// school.audit_as("创建学校").save(&ctx).await?;
227/// ```
228pub trait AuditedSaveExt {
229    fn save<'a>(
230        self,
231        ctx: &'a UserContext,
232    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
233}
234
235impl<T> AuditedSaveExt for teaql_core::Audited<T>
236where
237    T: Entity + Send + 'static,
238{
239    fn save<'a>(
240        self,
241        ctx: &'a UserContext,
242    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
243        Box::pin(async move {
244            let entity_name = T::entity_descriptor().name;
245            let entity = self.into_entity(); // applies comment onto the entity
246            let node = graph_node_from_entity(ctx, entity)?;
247            let saver = ctx
248                .require_resource::<Arc<dyn DynGraphSaver>>()
249                .map_err(|e| {
250                    RuntimeError::Graph(format!(
251                        "no DynGraphSaver registered — did you call register_executor()? ({})",
252                        e
253                    ))
254                })?;
255            saver.save_graph_dyn(ctx, node).await
256        })
257    }
258}
259
260/// Persist an audited generated entity, including pending ledger changes that
261/// may span multiple related entities sharing the same [`EntityRoot`](crate::EntityRoot).
262///
263/// Generated service crates use this as the implementation behind
264/// `entity.audit_as("why").save(&ctx)`. The audited wrapper is required by the
265/// function signature; no unaudited entity write entry point is exposed.
266#[doc(hidden)]
267pub async fn save_audited_ledger_entity<T>(
268    audited: teaql_core::Audited<T>,
269    ctx: &UserContext,
270) -> Result<GraphNode, RuntimeError>
271where
272    T: crate::LedgerEntity + Send + 'static,
273{
274    let entity_name = T::entity_descriptor().name;
275    let entity = audited.into_entity();
276    let root = entity.entity_root();
277    let node = graph_node_from_entity(ctx, entity)?;
278    let saver = ctx
279        .require_resource::<Arc<dyn DynGraphSaver>>()
280        .map_err(|e| {
281            RuntimeError::Graph(format!(
282                "no DynGraphSaver registered — did you call register_executor()? ({e})"
283            ))
284        })?;
285
286    if let Some(root) = root {
287        let has_ledger_changes = !root.current_change_set().changes().is_empty()
288            || !root.deleted_keys().is_empty()
289            || !root.new_keys().is_empty();
290        if has_ledger_changes {
291            return saver.save_ledger_dyn(ctx, node, root).await;
292        }
293    }
294
295    saver.save_graph_dyn(ctx, node).await
296}