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}