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    /// 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 draft = E::EdgeProvider::draft(ent);
74        let mut drafted_edges = draft.check(self)?;
75        drafted_edges.sort_by(|a, b| {
76            (&a.source, &a.sort_key).cmp(&(&b.source, &b.sort_key))
77        });
78
79        // Step 5: Check edge vector is same with one from step 1
80        if existing_edges != drafted_edges {
81            return Err(AuditError::EdgeMismatch {
82                existing: existing_edges,
83                drafted: drafted_edges,
84            });
85        }
86
87        // Step 6: Transaction will be dropped (not committed) - database unchanged
88        // The caller is responsible for not committing this transaction
89        Ok(())
90    }
91
92    fn fix_ent_edges<E: Ent>(self, id: Id) -> Result<(), AuditError>
93    where
94        Self: Sized,
95    {
96        // Step 1: Get the entity and verify it exists and is of correct type
97        let ent_box = self.get(id)?.ok_or(AuditError::EntityNotFound(id))?;
98
99        let ent = ent_box.as_ent::<E>().ok_or_else(|| {
100            AuditError::UnexpectedEntityType(
101                id,
102                std::any::type_name::<E>().to_string(),
103            )
104        })?;
105
106        // Step 2: Remove all incoming edges
107        self.remove_edges_by_dest(id)?;
108
109        // Step 3: Draft and create new edges
110        let draft = E::EdgeProvider::draft(ent);
111        let edges = draft.check(&self)?;
112
113        for edge in edges {
114            self.create_edge(edge)?;
115        }
116
117        self.commit()?;
118
119        Ok(())
120    }
121
122    /// List entities of a specific type with cursor-based pagination.
123    ///
124    /// # Arguments
125    /// * `entity_type` - The string name of the entity type to list
126    /// * `cursor` - Optional ID cursor. If provided, returns entities with ID > cursor
127    /// * `limit` - Maximum number of entities to return
128    fn list_entities(
129        &self,
130        entity_type: &str,
131        cursor: Option<Id>,
132        limit: usize,
133    ) -> Result<Vec<Box<dyn Ent>>, DatabaseError>;
134}