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 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
29/// Marker struct that implements [`DynGraphSaver`] for a specific executor type `E`.
30///
31/// `E` is the full executor type (e.g. `SqlDataServiceExecutor<SqliteDialect, …>`).
32/// The struct itself is zero-sized; the actual executor is retrieved from
33/// [`UserContext`] at call time.
34pub 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
72// ---------------------------------------------------------------------------
73// Standalone graph-node extraction (no executor needed)
74// ---------------------------------------------------------------------------
75
76/// Convert a typed entity into a [`GraphNode`] tree.
77///
78/// This only requires metadata (entity descriptors) from the [`UserContext`],
79/// **not** the database executor.  It is the standalone equivalent of
80/// [`EntityDataService::graph_node_from_entity`].
81pub 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
103/// Recursively convert a [`Record`] into a [`GraphNode`] tree.
104///
105/// Relations are resolved via the entity descriptors stored in `ctx`.
106fn 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
180// ---------------------------------------------------------------------------
181// AuditedSaveExt — the `.save(&ctx)` method on `Audited<T>`
182// ---------------------------------------------------------------------------
183
184/// Extension trait that provides the `.save(&ctx)` method on [`Audited<T>`](teaql_core::Audited).
185///
186/// # Example
187/// ```ignore
188/// use teaql_runtime::AuditedSaveExt;
189///
190/// school.audit_as("创建学校").save(&ctx).await?;
191/// ```
192pub 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(); // applies comment onto the entity
210            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}