ents_admin/
lib.rs

1use ents::{
2    DatabaseError, DraftError, Edge, EdgeDraft, EdgeValue, Ent, EntExt, Id,
3    IncomingEdgeProvider, Transactional,
4};
5
6/// Error type for edge audit operations.
7#[derive(Debug, thiserror::Error)]
8pub enum AuditError {
9    #[error("Entity not found: {0}")]
10    EntityNotFound(Id),
11
12    #[error("Unexpected entity type: {0} is not {1} type")]
13    UnexpectedEntityType(Id, String),
14
15    #[error("Edge mismatch: existing edges differ from drafted edges")]
16    EdgeMismatch {
17        existing: Vec<EdgeValue>,
18        drafted: Vec<EdgeValue>,
19    },
20
21    #[error("Draft error: {0}")]
22    Draft(#[from] DraftError),
23
24    #[error("Database error: {0}")]
25    Database(#[from] DatabaseError),
26}
27
28pub trait AdminEnt: Transactional {
29    fn find_edges_by_dest(&self, dest: Id) -> Result<Vec<Edge>, DatabaseError>;
30
31    fn remove_edges_by_dest(&self, dest: Id) -> Result<(), DatabaseError>;
32
33    fn audit_ent_edges<E: Ent>(&self, id: Id) -> Result<(), AuditError>
34    where
35        Self: Sized,
36    {
37        // Step 1: Get the entity and verify it exists and is of correct type
38        let ent_box = self.get(id)?.ok_or(AuditError::EntityNotFound(id))?;
39
40        let ent = ent_box.as_ent::<E>().ok_or_else(|| {
41            AuditError::UnexpectedEntityType(
42                id,
43                std::any::type_name::<E>().to_string(),
44            )
45        })?;
46
47        // Step 2: Find all edges whose dest is entity id
48        let mut existing_edges: Vec<EdgeValue> = self
49            .find_edges_by_dest(id)?
50            .into_iter()
51            .map(|e| EdgeValue::new(e.source, e.sort_key, e.dest))
52            .collect();
53        existing_edges.sort_by(|a, b| {
54            (&a.source, &a.sort_key).cmp(&(&b.source, &b.sort_key))
55        });
56
57        // Step 3: Remove all edges from step 1
58        self.remove_edges_by_dest(id)?;
59
60        // Step 4: Draft edges (this validates and creates new edge values)
61        let draft = E::EdgeProvider::draft(ent);
62        let mut drafted_edges = draft.check(self)?;
63        drafted_edges.sort_by(|a, b| {
64            (&a.source, &a.sort_key).cmp(&(&b.source, &b.sort_key))
65        });
66
67        // Step 5: Check edge vector is same with one from step 1
68        if existing_edges != drafted_edges {
69            return Err(AuditError::EdgeMismatch {
70                existing: existing_edges,
71                drafted: drafted_edges,
72            });
73        }
74
75        // Step 6: Transaction will be dropped (not committed) - database unchanged
76        // The caller is responsible for not committing this transaction
77        Ok(())
78    }
79
80    fn fix_ent_edges<E: Ent>(self, id: Id) -> Result<(), AuditError>
81    where
82        Self: Sized,
83    {
84        // Step 1: Get the entity and verify it exists and is of correct type
85        let ent_box = self.get(id)?.ok_or(AuditError::EntityNotFound(id))?;
86
87        let ent = ent_box.as_ent::<E>().ok_or_else(|| {
88            AuditError::UnexpectedEntityType(
89                id,
90                std::any::type_name::<E>().to_string(),
91            )
92        })?;
93
94        // Step 2: Remove all incoming edges
95        self.remove_edges_by_dest(id)?;
96
97        // Step 3: Draft and create new edges
98        let draft = E::EdgeProvider::draft(ent);
99        let edges = draft.check(&self)?;
100
101        for edge in edges {
102            self.create_edge(edge)?;
103        }
104
105        self.commit()?;
106
107        Ok(())
108    }
109
110    /// List entities of a specific type with cursor-based pagination.
111    ///
112    /// # Arguments
113    /// * `entity_type` - The string name of the entity type to list
114    /// * `cursor` - Optional ID cursor. If provided, returns entities with ID > cursor
115    /// * `limit` - Maximum number of entities to return
116    fn list_entities(
117        &self,
118        entity_type: &str,
119        cursor: Option<Id>,
120        limit: usize,
121    ) -> Result<Vec<Box<dyn Ent>>, DatabaseError>;
122}