Skip to main content

bear_mcp_server/
bear.rs

1use crate::Result;
2
3use serde::{Deserialize, Serialize};
4use sqlx::SqlitePool;
5use std::path::PathBuf;
6
7#[derive(sqlx::Type)]
8#[sqlx(transparent)]
9pub struct AppleCoreDateTime(f64);
10
11impl From<AppleCoreDateTime> for chrono::DateTime<chrono::Utc> {
12    fn from(value: AppleCoreDateTime) -> Self {
13        chrono::NaiveDateTime::new(
14            chrono::NaiveDate::from_ymd_opt(2001, 1, 1).expect("must succeed"),
15            chrono::NaiveTime::from_hms_opt(0, 0, 0).expect("must succeed"),
16        )
17        .and_utc()
18            + chrono::Duration::microseconds((value.0 * 1_000_000.0f64) as i64)
19    }
20}
21
22#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
23pub struct Note {
24    #[sqlx(try_from = "AppleCoreDateTime")]
25    creation_date: chrono::DateTime<chrono::Utc>,
26    subtitle: Option<String>,
27    text: Option<String>,
28    title: Option<String>,
29    id: String,
30}
31
32#[derive(sqlx::FromRow)]
33struct Tag {
34    title: String,
35}
36
37pub struct BearDatabase {
38    pool: SqlitePool,
39}
40
41impl BearDatabase {
42    pub async fn new(db_path: PathBuf) -> Result<Self> {
43        let database_url = format!("sqlite://{}?mode=ro", db_path.display());
44        let pool = SqlitePool::connect(&database_url).await?;
45        Ok(Self { pool })
46    }
47
48    pub async fn list_notes(&self, query: Option<&str>, tag: Option<&str>) -> Result<Vec<Note>> {
49        let search_pattern: Option<String> = query.map(|query| format!("%{query}%"));
50        let search_pattern_str = search_pattern.as_deref();
51
52        Ok(sqlx::query_as!(
53            Note,
54            "SELECT
55                notes.ZCREATIONDATE AS 'creation_date!: AppleCoreDateTime',
56                notes.ZSUBTITLE AS subtitle,
57                notes.ZTEXT AS text,
58                notes.ZTITLE AS title,
59                notes.ZUNIQUEIDENTIFIER AS 'id!'
60            FROM 
61                ZSFNOTE notes
62            LEFT JOIN 
63                Z_5TAGS note_tags ON notes.Z_PK = note_tags.Z_5NOTES
64            INNER JOIN 
65                ZSFNOTETAG tags ON tags.Z_PK = note_tags.Z_13TAGS
66            WHERE 
67                (? IS NULL OR tags.ZTITLE = ?)
68                AND (? IS NULL OR notes.ZTEXT LIKE ? OR notes.ZTITLE LIKE ?)",
69            tag,
70            tag,
71            search_pattern_str,
72            search_pattern_str,
73            search_pattern_str
74        )
75        .fetch_all(&self.pool)
76        .await?)
77    }
78
79    pub async fn list_tags(&self) -> Result<Vec<String>> {
80        Ok(sqlx::query_as!(
81            Tag,
82            "SELECT
83                ZTITLE AS 'title!'
84            FROM
85                ZSFNOTETAG"
86        )
87        .fetch_all(&self.pool)
88        .await?
89        .into_iter()
90        .map(|tag| tag.title)
91        .collect())
92    }
93}