Skip to main content

khive_storage/
entity.rs

1//! Entity storage capability — graph node CRUD.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use uuid::Uuid;
7
8use crate::types::{BatchWriteSummary, DeleteMode, Page, PageRequest, StorageResult};
9
10/// Storage-level entity record. Flat SQL-friendly representation.
11/// Maps to the `entities` substrate table.
12#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct Entity {
14    pub id: Uuid,
15    pub namespace: String,
16    pub kind: String,
17    /// Pack-governed subtype token. Maps to `entities.entity_type` column.
18    pub entity_type: Option<String>,
19    pub name: String,
20    pub description: Option<String>,
21    pub properties: Option<Value>,
22    pub tags: Vec<String>,
23    pub created_at: i64,
24    pub updated_at: i64,
25    pub deleted_at: Option<i64>,
26    /// When this entity was tombstoned by a merge, the `into` entity's ID.
27    pub merged_into: Option<Uuid>,
28    /// Opaque event ID for the merge that tombstoned this entity.
29    pub merge_event_id: Option<Uuid>,
30}
31
32impl Entity {
33    /// Create a new entity with a generated UUID and current timestamp.
34    pub fn new(
35        namespace: impl Into<String>,
36        kind: impl Into<String>,
37        name: impl Into<String>,
38    ) -> Self {
39        let now = chrono::Utc::now().timestamp_micros();
40        Self {
41            id: Uuid::new_v4(),
42            namespace: namespace.into(),
43            kind: kind.into(),
44            entity_type: None,
45            name: name.into(),
46            description: None,
47            properties: None,
48            tags: Vec::new(),
49            created_at: now,
50            updated_at: now,
51            deleted_at: None,
52            merged_into: None,
53            merge_event_id: None,
54        }
55    }
56
57    /// Set the pack-governed entity subtype token.
58    pub fn with_entity_type(mut self, t: Option<impl Into<String>>) -> Self {
59        self.entity_type = t.map(Into::into);
60        self
61    }
62
63    /// Set the entity description.
64    pub fn with_description(mut self, d: impl Into<String>) -> Self {
65        self.description = Some(d.into());
66        self
67    }
68
69    /// Set the entity properties JSON blob.
70    pub fn with_properties(mut self, p: Value) -> Self {
71        self.properties = Some(p);
72        self
73    }
74
75    /// Set the entity tags.
76    pub fn with_tags(mut self, t: Vec<String>) -> Self {
77        self.tags = t;
78        self
79    }
80}
81
82/// Entity filter for query operations.
83#[derive(Clone, Debug, Default, Serialize, Deserialize)]
84pub struct EntityFilter {
85    pub ids: Vec<Uuid>,
86    pub kinds: Vec<String>,
87    /// Filter by exact `entity_type` value. Multiple values are ORed.
88    pub entity_types: Vec<String>,
89    pub name_prefix: Option<String>,
90    pub tags_any: Vec<String>,
91    /// When non-empty, restricts results to any of these namespaces using
92    /// `namespace IN (...)`. Takes precedence over the `namespace` string
93    /// parameter passed to `query_entities` / `count_entities`. When empty the
94    /// caller-supplied `namespace` parameter is used (single-namespace path,
95    /// backward-compatible default).
96    #[serde(default)]
97    pub namespaces: Vec<String>,
98}
99
100/// Entity CRUD operations over the entities substrate table.
101#[async_trait]
102pub trait EntityStore: Send + Sync + 'static {
103    /// Insert or update a single entity.
104    async fn upsert_entity(&self, entity: Entity) -> StorageResult<()>;
105    /// Insert or update a batch of entities.
106    async fn upsert_entities(&self, entities: Vec<Entity>) -> StorageResult<BatchWriteSummary>;
107    /// Fetch an entity by UUID, returning `None` if absent.
108    async fn get_entity(&self, id: Uuid) -> StorageResult<Option<Entity>>;
109    /// Delete an entity by UUID using the specified delete mode.
110    async fn delete_entity(&self, id: Uuid, mode: DeleteMode) -> StorageResult<bool>;
111    /// Query entities by namespace with filter and pagination.
112    async fn query_entities(
113        &self,
114        namespace: &str,
115        filter: EntityFilter,
116        page: PageRequest,
117    ) -> StorageResult<Page<Entity>>;
118    /// Count entities in a namespace matching the given filter.
119    async fn count_entities(&self, namespace: &str, filter: EntityFilter) -> StorageResult<u64>;
120    /// Fetch an entity by UUID regardless of soft-deletion state.
121    ///
122    /// Returns the entity row even when `deleted_at` is set. Callers use this
123    /// to distinguish "soft-deleted" from "never existed".
124    async fn get_entity_including_deleted(&self, id: Uuid) -> StorageResult<Option<Entity>>;
125}