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        fn entity_descriptor() -> EntityDescriptor {
118            EntityDescriptor::new("Dummy")
119                .property(PropertyDescriptor::new("id", DataType::I64).id())
120        }
121    }
122
123    #[test]
124    fn test_entity_graph_builder_annotations_and_child_operations() {
125        let mut rec1 = BTreeMap::new();
126        rec1.insert("id".to_string(), Value::I64(1));
127        let entity1 = DummyEntity { record: rec1 };
128
129        let mut rec2 = BTreeMap::new();
130        rec2.insert("id".to_string(), Value::I64(2));
131        let entity2 = DummyEntity { record: rec2 };
132
133        let graph = EntityGraph::new(entity1)
134            .comment("Parent creation")
135            .child(
136                "dummy_items",
137                EntityGraph::new(entity2).comment("Child deletion").delete(),
138            )
139            .build();
140
141        let root = graph.root;
142        assert_eq!(root.entity_type, "Dummy");
143        assert_eq!(root.comment.as_deref(), Some("Parent creation"));
144        assert_eq!(root.operation, EntityGraphOperation::Save);
145        assert_eq!(root.children.len(), 1);
146
147        let (rel_name, child_node) = &root.children[0];
148        assert_eq!(rel_name, "dummy_items");
149        assert_eq!(child_node.entity_type, "Dummy");
150        assert_eq!(child_node.comment.as_deref(), Some("Child deletion"));
151        assert_eq!(child_node.operation, EntityGraphOperation::Delete);
152    }
153}