Skip to main content

khive_storage/
note.rs

1//! Note storage capability — temporal-referential record 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, SqlValue, StorageResult};
9
10/// A storage-level note record. Flat, SQL-friendly representation.
11#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct Note {
13    pub id: Uuid,
14    pub namespace: String,
15    pub kind: String,
16    pub status: String,
17    pub name: Option<String>,
18    pub content: String,
19    pub salience: Option<f64>,
20    pub decay_factor: Option<f64>,
21    pub expires_at: Option<i64>,
22    pub properties: Option<Value>,
23    pub created_at: i64,
24    pub updated_at: i64,
25    pub deleted_at: Option<i64>,
26}
27
28impl Note {
29    pub fn new(
30        namespace: impl Into<String>,
31        kind: impl Into<String>,
32        content: impl Into<String>,
33    ) -> Self {
34        let now = chrono::Utc::now().timestamp_micros();
35        Self {
36            id: Uuid::new_v4(),
37            namespace: namespace.into(),
38            kind: kind.into(),
39            status: "active".to_string(),
40            name: None,
41            content: content.into(),
42            salience: None,
43            decay_factor: None,
44            expires_at: None,
45            properties: None,
46            created_at: now,
47            updated_at: now,
48            deleted_at: None,
49        }
50    }
51
52    pub fn with_name(mut self, n: impl Into<String>) -> Self {
53        self.name = Some(n.into());
54        self
55    }
56
57    pub fn with_salience(mut self, s: f64) -> Self {
58        self.salience = Some(s.clamp(0.0, 1.0));
59        self
60    }
61
62    pub fn with_decay(mut self, d: f64) -> Self {
63        self.decay_factor = Some(d.max(0.0));
64        self
65    }
66
67    pub fn with_properties(mut self, p: Value) -> Self {
68        self.properties = Some(p);
69        self
70    }
71}
72
73/// Sort direction for filtered note queries.
74#[derive(Clone, Debug, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum SortDir {
77    Asc,
78    Desc,
79}
80
81/// Comparison operator for a [`PropertyFilter`] on a JSON path.
82#[derive(Clone, Debug, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum FilterOp {
85    Eq,
86    /// Matches rows where the JSON field equals the value OR the field is absent/NULL.
87    /// Used for properties that may be missing in legacy rows (e.g. `$.read`).
88    EqOrMissing,
89    Ne,
90    Lt,
91    Lte,
92    Gt,
93    Gte,
94}
95
96/// A single `json_extract(properties, '$.field') op value` predicate.
97///
98/// Callers import this as `khive_storage::note::PropertyFilter` to avoid
99/// collision with the vector-metadata `PropertyFilter` in `khive_storage::types`.
100#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct PropertyFilter {
102    pub json_path: String,
103    pub op: FilterOp,
104    pub value: SqlValue,
105}
106
107/// Filter + sort options for [`NoteStore::query_notes_filtered`].
108///
109/// Designed for general property-based filtering on any JSON field, not
110/// schedule-specific, so D9 and future packs can reuse the same API.
111#[derive(Clone, Debug, Default, Serialize, Deserialize)]
112pub struct NoteFilter {
113    pub kind: Option<String>,
114    #[serde(default)]
115    pub property_filters: Vec<PropertyFilter>,
116    /// `(json_path, direction)` — `None` defaults to `created_at DESC`.
117    pub order_by: Option<(String, SortDir)>,
118}
119
120#[async_trait]
121pub trait NoteStore: Send + Sync + 'static {
122    async fn upsert_note(&self, note: Note) -> StorageResult<()>;
123    async fn upsert_notes(&self, notes: Vec<Note>) -> StorageResult<BatchWriteSummary>;
124    async fn get_note(&self, id: Uuid) -> StorageResult<Option<Note>>;
125    async fn delete_note(&self, id: Uuid, mode: DeleteMode) -> StorageResult<bool>;
126    async fn query_notes(
127        &self,
128        namespace: &str,
129        kind: Option<&str>,
130        page: PageRequest,
131    ) -> StorageResult<Page<Note>>;
132    async fn query_notes_filtered(
133        &self,
134        namespace: &str,
135        filter: &NoteFilter,
136        page: PageRequest,
137    ) -> StorageResult<Page<Note>>;
138    async fn count_notes(&self, namespace: &str, kind: Option<&str>) -> StorageResult<u64>;
139
140    async fn get_notes_batch(&self, ids: &[Uuid]) -> StorageResult<Vec<Note>> {
141        let mut out = Vec::with_capacity(ids.len());
142        for &id in ids {
143            if let Some(n) = self.get_note(id).await? {
144                out.push(n);
145            }
146        }
147        Ok(out)
148    }
149}