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
28/// Admin trait for querying and manipulating edges by destination.
29/// This provides administrative operations for edge management.
30pub trait AdminEdgeByDest: Transactional {
31    fn find_edges_by_dest(&self, dest: Id) -> Result<Vec<Edge>, DatabaseError>;
32
33    fn remove_edges_by_dest(&self, dest: Id) -> Result<(), DatabaseError>;
34
35    fn audit_ent_edges<E: Ent>(&self, id: Id) -> Result<(), AuditError>
36    where
37        Self: Sized,
38    {
39        // Step 1: Get the entity and verify it exists and is of correct type
40        let ent_box = self.get(id)?.ok_or(AuditError::EntityNotFound(id))?;
41
42        let ent = ent_box.as_ent::<E>().ok_or_else(|| {
43            AuditError::UnexpectedEntityType(
44                id,
45                std::any::type_name::<E>().to_string(),
46            )
47        })?;
48
49        // Step 2: Find all edges whose dest is entity id
50        let mut existing_edges: Vec<EdgeValue> = self
51            .find_edges_by_dest(id)?
52            .into_iter()
53            .map(|e| EdgeValue::new(e.source, e.sort_key, e.dest))
54            .collect();
55        existing_edges.sort_by(|a, b| {
56            (&a.source, &a.sort_key).cmp(&(&b.source, &b.sort_key))
57        });
58
59        // Step 3: Remove all edges from step 1
60        self.remove_edges_by_dest(id)?;
61
62        // Step 4: Draft edges (this validates and creates new edge values)
63        let draft = E::EdgeProvider::draft(ent);
64        let mut drafted_edges = draft.check(self)?;
65        drafted_edges.sort_by(|a, b| {
66            (&a.source, &a.sort_key).cmp(&(&b.source, &b.sort_key))
67        });
68
69        // Step 5: Check edge vector is same with one from step 1
70        if existing_edges != drafted_edges {
71            return Err(AuditError::EdgeMismatch {
72                existing: existing_edges,
73                drafted: drafted_edges,
74            });
75        }
76
77        // Step 6: Transaction will be dropped (not committed) - database unchanged
78        // The caller is responsible for not committing this transaction
79        Ok(())
80    }
81
82    fn fix_ent_edges<E: Ent>(self, id: Id) -> Result<(), AuditError>
83    where
84        Self: Sized,
85    {
86        // Step 1: Get the entity and verify it exists and is of correct type
87        let ent_box = self.get(id)?.ok_or(AuditError::EntityNotFound(id))?;
88
89        let ent = ent_box.as_ent::<E>().ok_or_else(|| {
90            AuditError::UnexpectedEntityType(
91                id,
92                std::any::type_name::<E>().to_string(),
93            )
94        })?;
95
96        // Step 2: Remove all incoming edges
97        self.remove_edges_by_dest(id)?;
98
99        // Step 3: Draft and create new edges
100        let draft = E::EdgeProvider::draft(ent);
101        let edges = draft.check(&self)?;
102
103        for edge in edges {
104            self.create_edge(edge)?;
105        }
106
107        self.commit()?;
108
109        Ok(())
110    }
111}