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::{
10    DataServiceError, GraphNode, GraphOperation, RuntimeError, UserContext,
11};
12
13// ---------------------------------------------------------------------------
14// DynGraphSaver — type-erased graph save capability
15// ---------------------------------------------------------------------------
16
17/// Object-safe trait for saving a [`GraphNode`] tree to the database.
18///
19/// A concrete implementation is registered in [`UserContext`] during setup so
20/// that [`Audited::save`] can persist entities without exposing the underlying
21/// executor type to business code.
22pub 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
31/// Marker struct that implements [`DynGraphSaver`] for a specific executor type `E`.
32///
33/// `E` is the full executor type (e.g. `SqlDataServiceExecutor<SqliteDialect, …>`).
34/// The struct itself is zero-sized; the actual executor is retrieved from
35/// [`UserContext`] at call time.
36pub 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
74// ---------------------------------------------------------------------------
75// Standalone graph-node extraction (no executor needed)
76// ---------------------------------------------------------------------------
77
78/// Convert a typed entity into a [`GraphNode`] tree.
79///
80/// This only requires metadata (entity descriptors) from the [`UserContext`],
81/// **not** the database executor.  It is the standalone equivalent of
82/// [`EntityDataService::graph_node_from_entity`].
83pub 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
105/// Recursively convert a [`Record`] into a [`GraphNode`] tree.
106///
107/// Relations are resolved via the entity descriptors stored in `ctx`.
108fn 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
182// ---------------------------------------------------------------------------
183// AuditedSaveExt — the `.save(&ctx)` method on `Audited<T>`
184// ---------------------------------------------------------------------------
185
186/// Extension trait that provides the `.save(&ctx)` method on [`Audited<T>`](teaql_core::Audited).
187///
188/// # Example
189/// ```ignore
190/// use teaql_runtime::AuditedSaveExt;
191///
192/// school.audit_as("创建学校").save(&ctx).await?;
193/// ```
194pub 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(); // applies comment onto the entity
212            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}