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        entity: &'a str,
25        node: GraphNode,
26    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
27
28    fn save_ledger_dyn<'a>(
29        &'a self,
30        ctx: &'a UserContext,
31        entity: &'a str,
32        root: crate::EntityRoot,
33    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
34}
35
36/// Marker struct that implements [`DynGraphSaver`] for a specific executor type `E`.
37///
38/// `E` is the full executor type (e.g. `SqlDataServiceExecutor<SqliteDialect, …>`).
39/// The struct itself is zero-sized; the actual executor is retrieved from
40/// [`UserContext`] at call time.
41pub(crate) struct GraphSaverFor<E> {
42    _marker: PhantomData<fn() -> E>,
43}
44
45impl<E> GraphSaverFor<E> {
46    pub(crate) fn new() -> Self {
47        Self {
48            _marker: PhantomData,
49        }
50    }
51}
52
53impl<E> DynGraphSaver for GraphSaverFor<E>
54where
55    E: teaql_data_service::QueryExecutor
56        + teaql_data_service::MutationExecutor
57        + Send
58        + Sync
59        + 'static,
60{
61    fn save_graph_dyn<'a>(
62        &'a self,
63        ctx: &'a UserContext,
64        entity: &'a str,
65        node: GraphNode,
66    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
67        Box::pin(async move {
68            let eds = ctx
69                .entity_data_service::<E>(entity)
70                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
71            eds.save_graph_internal(node).await.map_err(|e| match e {
72                DataServiceError::Runtime(r) => r,
73                other => RuntimeError::Graph(other.to_string()),
74            })
75        })
76    }
77
78    fn save_ledger_dyn<'a>(
79        &'a self,
80        ctx: &'a UserContext,
81        entity: &'a str,
82        root: crate::EntityRoot,
83    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
84        Box::pin(async move {
85            let eds = ctx
86                .entity_data_service::<E>(entity)
87                .map_err(|e| RuntimeError::Graph(e.to_string()))?;
88            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            Ok(GraphNode::new(entity))
95        })
96    }
97}
98
99// ---------------------------------------------------------------------------
100// Standalone graph-node extraction (no executor needed)
101// ---------------------------------------------------------------------------
102
103/// Convert a typed entity into a [`GraphNode`] tree.
104///
105/// This only requires metadata (entity descriptors) from the [`UserContext`],
106/// **not** the database executor.  It is the standalone equivalent of
107/// [`EntityDataService::graph_node_from_entity`].
108pub fn graph_node_from_entity<T: Entity>(
109    ctx: &UserContext,
110    entity: T,
111) -> Result<GraphNode, RuntimeError> {
112    let descriptor = T::entity_descriptor();
113    let dirty_fields = entity.dirty_fields();
114    let original_values = entity.original_values();
115    let is_deleted = entity.is_marked_as_delete();
116    let comment = entity.get_comment();
117    let mut node = graph_node_from_record(ctx, &descriptor.name, entity.into_record())?;
118    node.dirty_fields = dirty_fields;
119    node.original_values = original_values;
120    if is_deleted {
121        node.operation = GraphOperation::Remove;
122        node.relations.clear();
123    }
124    if let Some(c) = comment {
125        node.set_comment(c);
126    }
127    Ok(node)
128}
129
130/// Recursively convert a [`Record`] into a [`GraphNode`] tree.
131///
132/// Relations are resolved via the entity descriptors stored in `ctx`.
133fn graph_node_from_record(
134    ctx: &UserContext,
135    entity: &str,
136    record: Record,
137) -> Result<GraphNode, RuntimeError> {
138    let descriptor = ctx.require_entity(entity)?;
139    let mut node = GraphNode::new(entity);
140
141    for (field, value) in record {
142        if field == "_comment" {
143            if let Value::Text(comment) = value {
144                node.set_comment(comment);
145            }
146            continue;
147        }
148        if field == "_dirty_fields" {
149            if let Value::List(fields) = value {
150                let mut dirty = BTreeSet::new();
151                for f in fields {
152                    if let Value::Text(t) = f {
153                        dirty.insert(t);
154                    }
155                }
156                node.dirty_fields = Some(dirty);
157            }
158            continue;
159        }
160        if field == "_original_values" {
161            if let Value::Object(orig) = value {
162                node.original_values = Some(orig);
163            }
164            continue;
165        }
166        let Some(relation) = descriptor.relation_by_name(&field) else {
167            node.values.insert(field, value);
168            continue;
169        };
170
171        match value {
172            Value::Null => {
173                node.relations.entry(field).or_default();
174            }
175            Value::Object(record) => {
176                let child = graph_node_from_record(ctx, &relation.target_entity, record)?;
177                node.relations.entry(field).or_default().push(child);
178            }
179            Value::List(values) => {
180                let children = node.relations.entry(field.clone()).or_default();
181                for value in values {
182                    let Value::Object(record) = value else {
183                        return Err(RuntimeError::Graph(format!(
184                            "relation {}.{} expects object children, got {:?}",
185                            entity, field, value
186                        )));
187                    };
188                    children.push(graph_node_from_record(
189                        ctx,
190                        &relation.target_entity,
191                        record,
192                    )?);
193                }
194            }
195            other => {
196                return Err(RuntimeError::Graph(format!(
197                    "relation {}.{} expects object/list/null, got {:?}",
198                    entity, field, other
199                )));
200            }
201        }
202    }
203
204    Ok(node)
205}
206
207// ---------------------------------------------------------------------------
208// AuditedSaveExt — the `.save(&ctx)` method on `Audited<T>`
209// ---------------------------------------------------------------------------
210
211/// Extension trait that provides the `.save(&ctx)` method on [`Audited<T>`](teaql_core::Audited).
212///
213/// # Example
214/// ```ignore
215/// use teaql_runtime::AuditedSaveExt;
216///
217/// school.audit_as("创建学校").save(&ctx).await?;
218/// ```
219pub trait AuditedSaveExt {
220    fn save<'a>(
221        self,
222        ctx: &'a UserContext,
223    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>>;
224}
225
226impl<T> AuditedSaveExt for teaql_core::Audited<T>
227where
228    T: Entity + Send + 'static,
229{
230    fn save<'a>(
231        self,
232        ctx: &'a UserContext,
233    ) -> Pin<Box<dyn Future<Output = Result<GraphNode, RuntimeError>> + Send + 'a>> {
234        Box::pin(async move {
235            let entity_name = T::entity_descriptor().name;
236            let entity = self.into_entity(); // applies comment onto the entity
237            let node = graph_node_from_entity(ctx, entity)?;
238            let saver = ctx
239                .require_resource::<Arc<dyn DynGraphSaver>>()
240                .map_err(|e| {
241                    RuntimeError::Graph(format!(
242                        "no DynGraphSaver registered — did you call register_executor()? ({})",
243                        e
244                    ))
245                })?;
246            saver.save_graph_dyn(ctx, &entity_name, node).await
247        })
248    }
249}
250
251/// Persist an audited generated entity, including pending ledger changes that
252/// may span multiple related entities sharing the same [`EntityRoot`](crate::EntityRoot).
253///
254/// Generated service crates use this as the implementation behind
255/// `entity.audit_as("why").save(&ctx)`. The audited wrapper is required by the
256/// function signature; no unaudited entity write entry point is exposed.
257#[doc(hidden)]
258pub async fn save_audited_ledger_entity<T>(
259    audited: teaql_core::Audited<T>,
260    ctx: &UserContext,
261) -> Result<GraphNode, RuntimeError>
262where
263    T: crate::LedgerEntity + Send + 'static,
264{
265    let entity_name = T::entity_descriptor().name;
266    let entity = audited.into_entity();
267    let root = entity.entity_root();
268    let node = graph_node_from_entity(ctx, entity)?;
269    let saver = ctx
270        .require_resource::<Arc<dyn DynGraphSaver>>()
271        .map_err(|e| {
272            RuntimeError::Graph(format!(
273                "no DynGraphSaver registered — did you call register_executor()? ({e})"
274            ))
275        })?;
276
277    if let Some(root) = root {
278        let has_ledger_changes = !root.current_change_set().changes().is_empty()
279            || !root.deleted_keys().is_empty()
280            || !root.new_keys().is_empty();
281        if has_ledger_changes {
282            return saver.save_ledger_dyn(ctx, &entity_name, root).await;
283        }
284    }
285
286    saver.save_graph_dyn(ctx, &entity_name, node).await
287}