diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index c9d96ec38..27b7e5540 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -302,26 +302,26 @@ impl App { self.files_database.list_by_project_id_session_id(project_id, session_id, paths) } - pub fn create_bookmark(&self, project_id: &str, timestamp_ms: &u128, note: &str) -> Result { + pub fn upsert_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result<()> { let gb_repository = gb_repository::Repository::open( self.local_data_dir.clone(), - project_id.to_string(), + bookmark.project_id.to_string(), self.projects_storage.clone(), self.users_storage.clone(), ) .context("failed to open repository")?; - let bookmark = bookmarks::Bookmark{ - id: uuid::Uuid::new_v4().to_string(), - project_id: project_id.to_string(), - timestamp_ms: *timestamp_ms, - note: note.to_string(), - }; - let session = gb_repository.get_or_create_current_session().context("failed to get or create current session")?; let writer = sessions::Writer::open(&gb_repository, &session).context("failed to open session writer")?; writer.write_bookmark(&bookmark).context("failed to write bookmark")?; - Ok(bookmark) + + self.proxy_watchers.lock().unwrap().get(&bookmark.project_id).map(|proxy_watcher| { + if let Err(e) = proxy_watcher.send(watcher::Event::Bookmark(bookmark.clone())) { + log::error!("failed to send bookmark event to proxy: {}", e); + } + }); + + Ok(()) } pub fn list_bookmarks(&self, project_id: &str, range: Option>) -> Result> { @@ -337,6 +337,7 @@ impl App { self.deltas_database.list_by_project_id_session_id(project_id, session_id, paths) } + pub fn git_activity( &self, project_id: &str, diff --git a/src-tauri/src/bookmarks/database.rs b/src-tauri/src/bookmarks/database.rs index c58241b84..51e60adca 100644 --- a/src-tauri/src/bookmarks/database.rs +++ b/src-tauri/src/bookmarks/database.rs @@ -16,15 +16,32 @@ impl Database { Self { database } } - pub fn insert(&self, bookmark: &Bookmark) -> Result<()> { + pub fn get_by_id(&self, id: &str) -> Result> { + self.database.transaction(|tx| { + let mut stmt = get_by_id_stmt(tx).context("Failed to prepare get_by_id statement")?; + let mut rows = stmt + .query(rusqlite::named_params! { ":id": id }) + .context("Failed to execute get_by_id statement")?; + if let Some(row) = rows.next()? { + Ok(Some(parse_row(row)?)) + } else { + Ok(None) + } + }) + } + + pub fn upsert(&self, bookmark: &Bookmark) -> Result<()> { self.database.transaction(|tx| -> Result<()> { let mut stmt = insert_stmt(tx).context("Failed to prepare insert statement")?; - let timestamp_ms = bookmark.timestamp_ms.to_string(); + let created_timestamp_ms = bookmark.created_timestamp_ms.to_string(); + let updated_timestamp_ms = bookmark.updated_timestamp_ms.to_string(); stmt.execute(rusqlite::named_params! { ":id": &bookmark.id, ":project_id": &bookmark.project_id, - ":timestamp_ms": ×tamp_ms, + ":created_timestamp_ms": &created_timestamp_ms, + ":updated_timestamp_ms": &updated_timestamp_ms, ":note": &bookmark.note, + ":deleted": &bookmark.deleted, }) .context("Failed to execute insert statement")?; Ok(()) @@ -87,8 +104,24 @@ fn insert_stmt<'conn>( ) -> Result> { Ok(tx.prepare_cached( " - INSERT INTO `bookmarks` (`id`, `project_id`, `timestamp_ms`, `note`) - VALUES (:id, :project_id, :timestamp_ms, :note) + INSERT INTO `bookmarks` (`id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted`) + VALUES (:id, :project_id, :created_timestamp_ms, :updated_timestamp_ms, :note, :deleted) + ON CONFLICT(`id`) DO UPDATE SET + `updated_timestamp_ms` = :updated_timestamp_ms, + `note` = :note, + `deleted` = :deleted + ", + )?) +} + +fn get_by_id_stmt<'conn>( + tx: &'conn rusqlite::Transaction, +) -> Result> { + Ok(tx.prepare_cached( + " + SELECT `id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted` + FROM `bookmarks` + WHERE `id` = :id ", )?) } @@ -98,12 +131,12 @@ fn list_by_project_id_range_stmt<'conn>( ) -> Result> { Ok(tx.prepare_cached( " - SELECT `id`, `project_id`, `timestamp_ms`, `note` + SELECT `id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted` FROM `bookmarks` WHERE `project_id` = :project_id - AND `timestamp_ms` >= :start - AND `timestamp_ms` < :end - ORDER BY `timestamp_ms` DESC + AND `updated_timestamp_ms` >= :start + AND `updated_timestamp_ms` < :end + ORDER BY `created_timestamp_ms` DESC ", )?) } @@ -113,10 +146,10 @@ fn list_by_project_id_stmt<'conn>( ) -> Result> { Ok(tx.prepare_cached( " - SELECT `id`, `project_id`, `timestamp_ms`, `note` + SELECT `id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted` FROM `bookmarks` WHERE `project_id` = :project_id - ORDER BY `timestamp_ms` DESC + ORDER BY `created_timestamp_ms` DESC ", )?) } @@ -125,12 +158,18 @@ fn parse_row(row: &rusqlite::Row) -> Result { Ok(Bookmark { id: row.get(0).context("Failed to get id")?, project_id: row.get(1).context("Failed to get project_id")?, - timestamp_ms: row + created_timestamp_ms: row .get::(2) - .context("Failed to get timestamp_ms")? - .parse() - .context("Failed to parse timestamp_ms")?, - note: row.get(3).context("Failed to get note")?, + .context("Failed to get created_timestamp_ms")? + .parse::() + .context("Failed to parse created_timestamp_ms")?, + updated_timestamp_ms: row + .get::(3) + .context("Failed to get updated_timestamp_ms")? + .parse::() + .context("Failed to parse updated_timestamp_ms")?, + note: row.get(4).context("Failed to get note")?, + deleted: row.get(5).context("Failed to get deleted")?, }) } @@ -146,11 +185,13 @@ mod tests { let bookmark = Bookmark { id: "id".to_string(), project_id: "project_id".to_string(), - timestamp_ms: 123, + created_timestamp_ms: 123, + updated_timestamp_ms: 123, note: "note".to_string(), + deleted: false, }; - database.insert(&bookmark)?; + database.upsert(&bookmark)?; let result = database.list_by_project_id_all(&bookmark.project_id)?; @@ -167,18 +208,22 @@ mod tests { let bookmark_one = Bookmark { id: "id".to_string(), project_id: "project_id".to_string(), - timestamp_ms: 123, + created_timestamp_ms: 123, + updated_timestamp_ms: 123, note: "note".to_string(), + deleted: false, }; - database.insert(&bookmark_one)?; + database.upsert(&bookmark_one)?; let bookmark_two = Bookmark { id: "id2".to_string(), project_id: "project_id".to_string(), - timestamp_ms: 456, + created_timestamp_ms: 456, + updated_timestamp_ms: 456, note: "note".to_string(), + deleted: false, }; - database.insert(&bookmark_two)?; + database.upsert(&bookmark_two)?; let result = database.list_by_project_id_range( &bookmark_one.project_id, @@ -188,4 +233,34 @@ mod tests { Ok(()) } + + #[test] + fn update() -> Result<()> { + let db = database::Database::memory()?; + let database = Database::new(db); + + assert_eq!(database.get_by_id("id")?, None); + + let bookmark = Bookmark { + id: "id".to_string(), + project_id: "project_id".to_string(), + created_timestamp_ms: 123, + updated_timestamp_ms: 123, + note: "note".to_string(), + deleted: false, + }; + + database.upsert(&bookmark)?; + assert_eq!(database.get_by_id(&bookmark.id)?, Some(bookmark.clone())); + + let updated = Bookmark { + note: "updated".to_string(), + updated_timestamp_ms: 456, + ..bookmark.clone() + }; + database.upsert(&updated)?; + assert_eq!(database.get_by_id(&bookmark.id.clone())?, Some(updated)); + + Ok(()) + } } diff --git a/src-tauri/src/bookmarks/mod.rs b/src-tauri/src/bookmarks/mod.rs index ec922c354..c88146122 100644 --- a/src-tauri/src/bookmarks/mod.rs +++ b/src-tauri/src/bookmarks/mod.rs @@ -3,13 +3,27 @@ mod reader; use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Bookmark { pub id: String, pub project_id: String, - pub timestamp_ms: u128, + pub created_timestamp_ms: u128, + pub updated_timestamp_ms: u128, pub note: String, + pub deleted: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Event { + Created(Bookmark), + Updated { + id: String, + note: Option, + deleted: Option, + timestamp_ms: u128, + }, } pub use database::Database; diff --git a/src-tauri/src/database/migrations/V4__bookmarks_update.sql b/src-tauri/src/database/migrations/V4__bookmarks_update.sql new file mode 100644 index 000000000..e068b765f --- /dev/null +++ b/src-tauri/src/database/migrations/V4__bookmarks_update.sql @@ -0,0 +1,16 @@ +ALTER TABLE `bookmarks` + ADD `created_timestamp_ms` text NOT NULL DEFAULT 0; + +UPDATE + `bookmarks` +SET + `created_timestamp_ms` = `timestamp_ms`; + +ALTER TABLE `bookmarks` + DROP COLUMN `timestamp_ms`; + +ALTER TABLE `bookmarks` + ADD `updated_timestamp_ms` text; + +ALTER TABLE `bookmarks` + ADD `deleted` boolean NOT NULL DEFAULT FALSE; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ec9b54b74..00b9e83a4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -486,10 +486,10 @@ async fn delete_all_data(handle: tauri::AppHandle) -> Result<(), Error> { #[timed(duration(printer = "debug!"))] #[tauri::command(async)] -async fn create_bookmark(handle: tauri::AppHandle, project_id: &str, timestamp_ms: u128, note: &str) -> Result { +async fn upsert_bookmark(handle: tauri::AppHandle, bookmark: bookmarks::Bookmark) -> Result<(), Error> { let app = handle.state::(); - let bookmark = app.create_bookmark(project_id, ×tamp_ms, note).context("failed to create bookmark")?; - Ok(bookmark) + app.upsert_bookmark(&bookmark).context("failed to upsert bookmark")?; + Ok(()) } #[timed(duration(printer = "debug!"))] @@ -638,7 +638,7 @@ fn main() { get_logs_archive_path, get_project_archive_path, get_project_data_archive_path, - create_bookmark, + upsert_bookmark, list_bookmarks, ]) .build(tauri_context) diff --git a/src-tauri/src/watcher/events.rs b/src-tauri/src/watcher/events.rs index c2f6bd6d4..b1a5b5a49 100644 --- a/src-tauri/src/watcher/events.rs +++ b/src-tauri/src/watcher/events.rs @@ -1,6 +1,6 @@ use std::{path, time}; -use crate::{deltas, sessions}; +use crate::{bookmarks, deltas, sessions}; pub enum Event { Tick(time::SystemTime), @@ -17,6 +17,7 @@ pub enum Event { ProjectFileChange(path::PathBuf), Session(sessions::Session), + Bookmark(bookmarks::Bookmark), File((String, path::PathBuf, String)), Deltas((String, path::PathBuf, Vec)), } diff --git a/src-tauri/src/watcher/handlers/index_handler.rs b/src-tauri/src/watcher/handlers/index_handler.rs index 6f43153ac..4473aa481 100644 --- a/src-tauri/src/watcher/handlers/index_handler.rs +++ b/src-tauri/src/watcher/handlers/index_handler.rs @@ -60,7 +60,14 @@ impl<'handler> Handler<'handler> { } pub fn index_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result> { - self.bookmarks_database.insert(&bookmark)?; + match self.bookmarks_database.get_by_id(&bookmark.id)? { + Some(existing) => { + if existing.updated_timestamp_ms < bookmark.updated_timestamp_ms { + self.bookmarks_database.upsert(&bookmark)?; + } + } + None => self.bookmarks_database.upsert(&bookmark)?, + } Ok(vec![]) } diff --git a/src-tauri/src/watcher/handlers/mod.rs b/src-tauri/src/watcher/handlers/mod.rs index cea39350d..160e07655 100644 --- a/src-tauri/src/watcher/handlers/mod.rs +++ b/src-tauri/src/watcher/handlers/mod.rs @@ -139,7 +139,8 @@ impl<'handler> Handler<'handler> { events::Event::Fetch => self.fetch_project_handler.handle(), events::Event::File((session_id, file_path, contents)) => { - self.index_handler + let file_events = self + .index_handler .index_file(&session_id, file_path.to_str().unwrap(), &contents) .context("failed to index file")?; self.events_sender @@ -150,19 +151,21 @@ impl<'handler> Handler<'handler> { &contents, )) .context("failed to send file event")?; - Ok(vec![]) + Ok(file_events) } events::Event::Session(session) => { - self.index_handler + let session_events = self + .index_handler .index_session(&session) .context("failed to index session")?; self.events_sender .send(app_events::Event::session(&self.project_id, &session)) .context("failed to send session event")?; - Ok(vec![]) + Ok(session_events) } events::Event::Deltas((session_id, path, deltas)) => { - self.index_handler + let delta_events = self + .index_handler .index_deltas(&session_id, path.to_str().unwrap(), &deltas) .context("failed to index deltas")?; self.events_sender @@ -173,8 +176,12 @@ impl<'handler> Handler<'handler> { &path, )) .context("failed to send deltas event")?; - Ok(vec![]) + Ok(delta_events) } + events::Event::Bookmark(bookmark) => self + .index_handler + .index_bookmark(&bookmark) + .context("failed to index bookmark"), } } } diff --git a/src/lib/api/ipc/bookmarks.ts b/src/lib/api/ipc/bookmarks.ts new file mode 100644 index 000000000..31fd85ac9 --- /dev/null +++ b/src/lib/api/ipc/bookmarks.ts @@ -0,0 +1,12 @@ +import { invoke } from '$lib/ipc'; + +export type Bookmark = { + id: string; + projectId: string; + createdTimestampMs: number; + updatedTimestampMs: number; + note: string; + deleted: boolean; +}; + +export const upsert = (params: { bookmark: Bookmark }) => invoke('upsert_bookmark', params); diff --git a/src/lib/api/ipc/index.ts b/src/lib/api/ipc/index.ts index 13be7dd19..d84e80dbb 100644 --- a/src/lib/api/ipc/index.ts +++ b/src/lib/api/ipc/index.ts @@ -11,6 +11,7 @@ export * as searchResults from './search'; export { type SearchResult } from './search'; export * as files from './files'; export * as zip from './zip'; +export * as bookmarks from './bookmarks'; import { invoke } from '$lib/ipc';