Skip to main content

vs_store/
types.rs

1//! Domain types — owned, plain Rust mirrors of the SQLite tables.
2//!
3//! Conversions to/from `rusqlite::Row` live alongside each type so the
4//! query bodies elsewhere in the crate stay short.
5
6use std::fmt;
7use std::str::FromStr;
8
9use rusqlite::{Row, ToSql};
10
11use crate::error::{Result, StoreError};
12
13/// Lifecycle of a session.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SessionStatus {
16    Open,
17    Closed,
18}
19
20impl SessionStatus {
21    #[must_use]
22    pub const fn as_str(self) -> &'static str {
23        match self {
24            Self::Open => "open",
25            Self::Closed => "closed",
26        }
27    }
28}
29
30impl fmt::Display for SessionStatus {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        f.write_str(self.as_str())
33    }
34}
35
36impl FromStr for SessionStatus {
37    type Err = StoreError;
38    fn from_str(s: &str) -> Result<Self> {
39        match s {
40            "open" => Ok(Self::Open),
41            "closed" => Ok(Self::Closed),
42            _ => Err(StoreError::Invalid("session.status")),
43        }
44    }
45}
46
47impl ToSql for SessionStatus {
48    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
49        Ok(rusqlite::types::ToSqlOutput::from(self.as_str()))
50    }
51}
52
53/// Row of `sessions`.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Session {
56    pub id: String,
57    pub created_at: i64,
58    pub policy_id: Option<String>,
59    pub status: SessionStatus,
60    pub closed_at: Option<i64>,
61}
62
63impl Session {
64    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
65        let status_str: String = row.get("status")?;
66        Ok(Self {
67            id: row.get("id")?,
68            created_at: row.get("created_at")?,
69            policy_id: row.get("policy_id")?,
70            status: status_str.parse().map_err(|_| {
71                rusqlite::Error::FromSqlConversionFailure(
72                    0,
73                    rusqlite::types::Type::Text,
74                    "session.status".into(),
75                )
76            })?,
77            closed_at: row.get("closed_at")?,
78        })
79    }
80}
81
82/// Row of `pages`.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Page {
85    pub id: String,
86    pub session_id: String,
87    pub url: String,
88    pub title: Option<String>,
89    pub last_token: Option<String>,
90    pub last_dom_hash: Option<String>,
91    pub last_seen_at: i64,
92    pub closed_at: Option<i64>,
93}
94
95impl Page {
96    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
97        Ok(Self {
98            id: row.get("id")?,
99            session_id: row.get("session_id")?,
100            url: row.get("url")?,
101            title: row.get("title")?,
102            last_token: row.get("last_token")?,
103            last_dom_hash: row.get("last_dom_hash")?,
104            last_seen_at: row.get("last_seen_at")?,
105            closed_at: row.get("closed_at")?,
106        })
107    }
108}
109
110/// Row of `refs`.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct StoredRef {
113    pub session_id: String,
114    pub page_id: String,
115    pub r: u32,
116    pub dom_path: String,
117    pub role: String,
118    pub content_hash: String,
119    pub created_at: i64,
120    pub retired_at: Option<i64>,
121}
122
123impl StoredRef {
124    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
125        let r_i64: i64 = row.get("ref")?;
126        Ok(Self {
127            session_id: row.get("session_id")?,
128            page_id: row.get("page_id")?,
129            r: u32::try_from(r_i64).map_err(|_| {
130                rusqlite::Error::FromSqlConversionFailure(
131                    0,
132                    rusqlite::types::Type::Integer,
133                    "ref out of u32 range".into(),
134                )
135            })?,
136            dom_path: row.get("dom_path")?,
137            role: row.get("role")?,
138            content_hash: row.get("content_hash")?,
139            created_at: row.get("created_at")?,
140            retired_at: row.get("retired_at")?,
141        })
142    }
143}
144
145/// Row of `marks`.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Mark {
148    pub id: String,
149    pub session_id: String,
150    pub page_id: String,
151    pub name: String,
152    pub dom_path: String,
153    pub role: Option<String>,
154    pub content_excerpt: Option<String>,
155    pub created_at: i64,
156}
157
158impl Mark {
159    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
160        Ok(Self {
161            id: row.get("id")?,
162            session_id: row.get("session_id")?,
163            page_id: row.get("page_id")?,
164            name: row.get("name")?,
165            dom_path: row.get("dom_path")?,
166            role: row.get("role")?,
167            content_excerpt: row.get("content_excerpt")?,
168            created_at: row.get("created_at")?,
169        })
170    }
171}
172
173/// What an annotation targets.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum AnnotationTarget {
176    Ref(u32),
177    Mark(String),
178    Page,
179}
180
181impl AnnotationTarget {
182    #[must_use]
183    pub fn kind(&self) -> &'static str {
184        match self {
185            Self::Ref(_) => "ref",
186            Self::Mark(_) => "mark",
187            Self::Page => "page",
188        }
189    }
190
191    #[must_use]
192    pub fn id(&self) -> String {
193        match self {
194            Self::Ref(r) => r.to_string(),
195            Self::Mark(name) => name.clone(),
196            Self::Page => String::new(),
197        }
198    }
199
200    pub(crate) fn parse(kind: &str, id: &str) -> Result<Self> {
201        match kind {
202            "ref" => {
203                let r: u32 = id
204                    .parse()
205                    .map_err(|_| StoreError::Invalid("annotation.target_id (ref)"))?;
206                Ok(Self::Ref(r))
207            }
208            "mark" => Ok(Self::Mark(id.to_string())),
209            "page" => Ok(Self::Page),
210            _ => Err(StoreError::Invalid("annotation.target_kind")),
211        }
212    }
213}
214
215/// Row of `annotations`.
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct Annotation {
218    pub id: String,
219    pub target: AnnotationTarget,
220    pub key: String,
221    pub value: Option<String>,
222    pub created_at: i64,
223}
224
225impl Annotation {
226    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
227        let kind: String = row.get("target_kind")?;
228        let id: String = row.get("target_id")?;
229        let target = AnnotationTarget::parse(&kind, &id).map_err(|_| {
230            rusqlite::Error::FromSqlConversionFailure(
231                0,
232                rusqlite::types::Type::Text,
233                "annotation.target".into(),
234            )
235        })?;
236        Ok(Self {
237            id: row.get("id")?,
238            target,
239            key: row.get("key")?,
240            value: row.get("value")?,
241            created_at: row.get("created_at")?,
242        })
243    }
244}
245
246/// Row of `actions`. Auditable record of one primitive call.
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct Action {
249    pub id: i64,
250    pub session_id: String,
251    pub page_id: Option<String>,
252    pub primitive: String,
253    pub args_redacted: String,
254    pub args_hash: String,
255    pub before_token: Option<String>,
256    pub after_token: Option<String>,
257    pub idempotency_hit: bool,
258    pub result_summary: Option<String>,
259    pub latency_ms: i64,
260    pub group_label: Option<String>,
261    pub started_at: i64,
262    pub finished_at: i64,
263    pub error_code: Option<String>,
264}
265
266impl Action {
267    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
268        let idem: i64 = row.get("idempotency_hit")?;
269        Ok(Self {
270            id: row.get("id")?,
271            session_id: row.get("session_id")?,
272            page_id: row.get("page_id")?,
273            primitive: row.get("primitive")?,
274            args_redacted: row.get("args_redacted")?,
275            args_hash: row.get("args_hash")?,
276            before_token: row.get("before_token")?,
277            after_token: row.get("after_token")?,
278            idempotency_hit: idem != 0,
279            result_summary: row.get("result_summary")?,
280            latency_ms: row.get("latency_ms")?,
281            group_label: row.get("group_label")?,
282            started_at: row.get("started_at")?,
283            finished_at: row.get("finished_at")?,
284            error_code: row.get("error_code")?,
285        })
286    }
287}
288
289/// What [`Store::record_action`](crate::Store::record_action) takes.
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct ActionInsert {
292    pub session_id: String,
293    pub page_id: Option<String>,
294    pub primitive: String,
295    pub args_redacted: String,
296    pub args_hash: String,
297    pub before_token: Option<String>,
298    pub after_token: Option<String>,
299    pub idempotency_hit: bool,
300    pub result_summary: Option<String>,
301    pub latency_ms: i64,
302    pub group_label: Option<String>,
303    pub started_at: i64,
304    pub finished_at: i64,
305    pub error_code: Option<String>,
306}
307
308/// Filter for [`Store::list_actions`](crate::Store::list_actions).
309#[derive(Debug, Clone, Default)]
310pub struct ActionFilter {
311    pub session_id: Option<String>,
312    pub page_id: Option<String>,
313    pub group_label: Option<String>,
314    pub since_started_at: Option<i64>,
315    pub limit: Option<i64>,
316}
317
318/// Metadata for a stored auth blob.
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct AuthBlobMeta {
321    pub name: String,
322    pub created_at: i64,
323    pub last_used_at: Option<i64>,
324}
325
326impl AuthBlobMeta {
327    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
328        Ok(Self {
329            name: row.get("name")?,
330            created_at: row.get("created_at")?,
331            last_used_at: row.get("last_used_at")?,
332        })
333    }
334}
335
336/// Row of `skill_cache`.
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct SkillEntry {
339    pub name: String,
340    pub version: String,
341    pub sha: String,
342    pub manifest: String,
343    pub last_used_at: Option<i64>,
344}
345
346impl SkillEntry {
347    pub(crate) fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
348        Ok(Self {
349            name: row.get("name")?,
350            version: row.get("version")?,
351            sha: row.get("sha")?,
352            manifest: row.get("manifest")?,
353            last_used_at: row.get("last_used_at")?,
354        })
355    }
356}