Skip to main content

teaql_core/
entity_graph.rs

1use crate::{Entity, Record, TeaqlEntity};
2
3/// Operation hint for an entity graph node.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum EntityGraphOperation {
6    /// Upsert: insert if new, update if exists (default).
7    Save,
8    /// Delete: soft-delete the entity.
9    Delete,
10}
11
12/// A single node in an annotated entity graph.
13///
14/// Carries the entity's record data, an optional business-intent comment,
15/// and child nodes keyed by relation name.
16#[derive(Debug, Clone)]
17pub struct EntityGraphNode {
18    pub entity_type: String,
19    pub record: Record,
20    pub comment: Option<String>,
21    pub operation: EntityGraphOperation,
22    pub children: Vec<(String, EntityGraphNode)>,
23}
24
25/// Builder for constructing an annotated entity graph that preserves
26/// comment trace chains through the save pipeline.
27///
28/// # Example
29///
30/// ```ignore
31/// let graph = EntityGraph::new(task)
32///     .comment("Create task 'Deploy v2'")
33///     .child("task_execution_log_list",
34///         EntityGraph::new(log)
35///             .comment("Create task 'Deploy v2'"))
36///     .build();
37/// ```
38pub struct EntityGraphBuilder {
39    node: EntityGraphNode,
40}
41
42impl EntityGraphBuilder {
43    /// Set a business-intent comment on this node.
44    /// The comment will appear in SQL debug logs and audit trails
45    /// as part of the hierarchical trace chain.
46    pub fn comment(mut self, comment: impl Into<String>) -> Self {
47        self.node.comment = Some(comment.into());
48        self
49    }
50
51    /// Mark this node for deletion instead of save.
52    pub fn delete(mut self) -> Self {
53        self.node.operation = EntityGraphOperation::Delete;
54        self
55    }
56
57    /// Attach a child entity under the given relation name.
58    pub fn child(mut self, relation: impl Into<String>, child: EntityGraphBuilder) -> Self {
59        self.node.children.push((relation.into(), child.node));
60        self
61    }
62
63    /// Finalize and produce the `EntityGraph`.
64    pub fn build(self) -> EntityGraph {
65        EntityGraph { root: self.node }
66    }
67}
68
69/// An annotated entity graph ready for saving.
70///
71/// Unlike raw `entity.save()`, this structure preserves business-intent
72/// comments at every hop in the graph, producing proper trace chains
73/// in SQL logs and audit trails.
74pub struct EntityGraph {
75    pub root: EntityGraphNode,
76}
77
78impl EntityGraph {
79    /// Start building from an entity.
80    #[allow(clippy::new_ret_no_self)]
81    pub fn new<T: Entity + TeaqlEntity>(entity: T) -> EntityGraphBuilder {
82        EntityGraphBuilder {
83            node: EntityGraphNode {
84                entity_type: T::entity_descriptor().name.clone(),
85                record: entity.into_record(),
86                comment: None,
87                operation: EntityGraphOperation::Save,
88                children: Vec::new(),
89            },
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::{DataType, EntityDescriptor, EntityError, PropertyDescriptor, Value};
98    use std::collections::BTreeMap;
99
100    // A dummy entity for testing
101    #[derive(Debug, Clone)]
102    struct DummyEntity {
103        record: Record,
104    }
105
106    impl Entity for DummyEntity {
107        fn into_record(self) -> Record {
108            self.record
109        }
110
111        fn from_record(record: Record) -> Result<Self, EntityError> {
112            Ok(Self { record })
113        }
114    }
115
116    impl TeaqlEntity for DummyEntity {
117        const ENTITY_NAME: &'static str = "Dummy";
118
119        fn entity_descriptor() -> EntityDescriptor {
120            EntityDescriptor::new("Dummy")
121                .property(PropertyDescriptor::new("id", DataType::I64).id())
122        }
123    }
124
125    #[test]
126    fn test_entity_graph_builder_annotations_and_child_operations() {
127        let mut rec1 = BTreeMap::new();
128        rec1.insert("id".to_string(), Value::I64(1));
129        let entity1 = DummyEntity { record: rec1 };
130
131        let mut rec2 = BTreeMap::new();
132        rec2.insert("id".to_string(), Value::I64(2));
133        let entity2 = DummyEntity { record: rec2 };
134
135        let graph = EntityGraph::new(entity1)
136            .comment("Parent creation")
137            .child(
138                "dummy_items",
139                EntityGraph::new(entity2).comment("Child deletion").delete(),
140            )
141            .build();
142
143        let root = graph.root;
144        assert_eq!(root.entity_type, "Dummy");
145        assert_eq!(root.comment.as_deref(), Some("Parent creation"));
146        assert_eq!(root.operation, EntityGraphOperation::Save);
147        assert_eq!(root.children.len(), 1);
148
149        let (rel_name, child_node) = &root.children[0];
150        assert_eq!(rel_name, "dummy_items");
151        assert_eq!(child_node.entity_type, "Dummy");
152        assert_eq!(child_node.comment.as_deref(), Some("Child deletion"));
153        assert_eq!(child_node.operation, EntityGraphOperation::Delete);
154    }
155}