Skip to main content

ents_admin/
lib.rs

1use ents::{
2    check_incoming_edges, DatabaseError, DraftError, Edge, EdgeValue, Ent,
3    EntExt, Id, 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    /// Create an entity from a boxed dynamic Ent.
34    ///
35    /// This is used by the admin API to create entities without knowing their
36    /// concrete type at compile time.
37    fn create_dyn(&self, ent: Box<dyn Ent>) -> Result<Id, DatabaseError>;
38
39    /// Update an entity from a boxed dynamic Ent.
40    ///
41    /// The entity's ID should already be set. This method replaces the
42    /// existing entity data with the provided entity.
43    fn update_dyn(&self, ent: Box<dyn Ent>) -> Result<(), DatabaseError>;
44
45    fn audit_ent_edges<E: Ent>(&self, id: Id) -> Result<(), AuditError>
46    where
47        Self: Sized,
48    {
49        // Step 1: Get the entity and verify it exists and is of correct type
50        let ent_box = self.get(id)?.ok_or(AuditError::EntityNotFound(id))?;
51
52        let ent = ent_box.as_ent::<E>().ok_or_else(|| {
53            AuditError::UnexpectedEntityType(
54                id,
55                std::any::type_name::<E>().to_string(),
56            )
57        })?;
58
59        // Step 2: Find all edges whose dest is entity id
60        let mut existing_edges: Vec<EdgeValue> = self
61            .find_edges_by_dest(id)?
62            .into_iter()
63            .map(|e| EdgeValue::new(e.source, e.sort_key, e.dest))
64            .collect();
65        existing_edges.sort_by(|a, b| {
66            (&a.source, &a.sort_key).cmp(&(&b.source, &b.sort_key))
67        });
68
69        // Step 3: Remove all edges from step 1
70        self.remove_edges_by_dest(id)?;
71
72        // Step 4: Draft edges (this validates and creates new edge values)
73        let mut drafted_edges = check_incoming_edges(ent, self)?;
74        drafted_edges.sort_by(|a, b| {
75            (&a.source, &a.sort_key).cmp(&(&b.source, &b.sort_key))
76        });
77
78        // Step 5: Check edge vector is same with one from step 1
79        if existing_edges != drafted_edges {
80            return Err(AuditError::EdgeMismatch {
81                existing: existing_edges,
82                drafted: drafted_edges,
83            });
84        }
85
86        // Step 6: Transaction will be dropped (not committed) - database unchanged
87        // The caller is responsible for not committing this transaction
88        Ok(())
89    }
90
91    fn fix_ent_edges<E: Ent>(self, id: Id) -> Result<(), AuditError>
92    where
93        Self: Sized,
94    {
95        // Step 1: Get the entity and verify it exists and is of correct type
96        let ent_box = self.get(id)?.ok_or(AuditError::EntityNotFound(id))?;
97
98        let ent = ent_box.as_ent::<E>().ok_or_else(|| {
99            AuditError::UnexpectedEntityType(
100                id,
101                std::any::type_name::<E>().to_string(),
102            )
103        })?;
104
105        // Step 2: Remove all incoming edges
106        self.remove_edges_by_dest(id)?;
107
108        // Step 3: Draft and create new edges
109        let edges = check_incoming_edges(ent, &self)?;
110
111        for edge in edges {
112            self.create_edge(edge)?;
113        }
114
115        self.commit()?;
116
117        Ok(())
118    }
119
120    /// List entities of a specific type with cursor-based pagination.
121    ///
122    /// # Arguments
123    /// * `entity_type` - The string name of the entity type to list
124    /// * `cursor` - Optional ID cursor. If provided, returns entities with ID > cursor
125    /// * `limit` - Maximum number of entities to return
126    fn list_entities(
127        &self,
128        entity_type: &str,
129        cursor: Option<Id>,
130        limit: usize,
131    ) -> Result<Vec<Box<dyn Ent>>, DatabaseError>;
132}