vs-store 0.1.4

SQLite-backed durable state for vibesurfer.
Documentation
//! `marks` table CRUD.

use rusqlite::params;

use super::{epoch_secs, Store};
use crate::error::{Result, StoreError};
use crate::types::Mark;

impl Store {
    #[allow(clippy::too_many_arguments)]
    pub fn create_mark(
        &mut self,
        id: &str,
        session_id: &str,
        page_id: &str,
        name: &str,
        dom_path: &str,
        role: Option<&str>,
        content_excerpt: Option<&str>,
    ) -> Result<Mark> {
        let now = epoch_secs();
        self.conn()
            .execute(
                "INSERT INTO marks(id, session_id, page_id, name, dom_path,
                                   role, content_excerpt, created_at)
                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
                params![
                    id,
                    session_id,
                    page_id,
                    name,
                    dom_path,
                    role,
                    content_excerpt,
                    now
                ],
            )
            .map_err(|e| match e {
                rusqlite::Error::SqliteFailure(err, _)
                    if err.code == rusqlite::ErrorCode::ConstraintViolation =>
                {
                    StoreError::Conflict("mark name already in session")
                }
                other => StoreError::Sqlite(other),
            })?;
        Ok(Mark {
            id: id.to_string(),
            session_id: session_id.to_string(),
            page_id: page_id.to_string(),
            name: name.to_string(),
            dom_path: dom_path.to_string(),
            role: role.map(str::to_string),
            content_excerpt: content_excerpt.map(str::to_string),
            created_at: now,
        })
    }

    pub fn get_mark(&self, session_id: &str, name: &str) -> Result<Option<Mark>> {
        let mut stmt = self
            .conn()
            .prepare("SELECT * FROM marks WHERE session_id=?1 AND name=?2")?;
        let mut rows = stmt.query([session_id, name])?;
        if let Some(row) = rows.next()? {
            Ok(Some(Mark::from_row(row)?))
        } else {
            Ok(None)
        }
    }

    pub fn list_marks(&self, session_id: &str) -> Result<Vec<Mark>> {
        let mut stmt = self
            .conn()
            .prepare("SELECT * FROM marks WHERE session_id=?1 ORDER BY created_at ASC")?;
        let rows = stmt.query_map([session_id], Mark::from_row)?;
        Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
    }

    pub fn delete_mark(&mut self, session_id: &str, name: &str) -> Result<()> {
        let n = self.conn().execute(
            "DELETE FROM marks WHERE session_id=?1 AND name=?2",
            params![session_id, name],
        )?;
        if n == 0 {
            return Err(StoreError::NotFound {
                kind: "mark",
                id: format!("{session_id}/{name}"),
            });
        }
        Ok(())
    }
}