diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 16b8805..620abdf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -307,8 +319,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -714,6 +728,18 @@ dependencies = [ "typeid", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.4.1" @@ -1209,6 +1235,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -1224,6 +1259,15 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -1728,6 +1772,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" version = "0.8.2" @@ -2078,10 +2133,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" name = "orchai" version = "0.1.0" dependencies = [ + "chrono", + "rusqlite", "serde", "serde_json", "tauri", "tauri-build", + "uuid", ] [[package]] @@ -2691,6 +2749,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3939,6 +4011,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a61e2b4..5035723 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,9 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" +rusqlite = { version = "0.31", features = ["bundled"] } +uuid = { version = "1", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } [profile.dev] incremental = true # Compiles your binary in smaller steps. diff --git a/src-tauri/migrations/001_init.sql b/src-tauri/migrations/001_init.sql new file mode 100644 index 0000000..68a33e0 --- /dev/null +++ b/src-tauri/migrations/001_init.sql @@ -0,0 +1,61 @@ +CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + cloned_from TEXT, + base_branch TEXT NOT NULL DEFAULT 'main', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS tuleap_credentials ( + id TEXT PRIMARY KEY, + tuleap_url TEXT NOT NULL, + username TEXT NOT NULL, + password_encrypted TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS watched_trackers ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + tracker_id INTEGER NOT NULL, + tracker_label TEXT NOT NULL, + polling_interval INTEGER NOT NULL DEFAULT 10, + agent_config_json TEXT NOT NULL, + filters_json TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS processed_tickets ( + id TEXT PRIMARY KEY, + tracker_id TEXT NOT NULL REFERENCES watched_trackers(id) ON DELETE CASCADE, + artifact_id INTEGER NOT NULL, + artifact_title TEXT NOT NULL, + artifact_data TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'Pending', + analyst_report TEXT, + developer_report TEXT, + detected_at TEXT NOT NULL DEFAULT (datetime('now')), + processed_at TEXT +); + +CREATE TABLE IF NOT EXISTS worktrees ( + id TEXT PRIMARY KEY, + ticket_id TEXT NOT NULL REFERENCES processed_tickets(id), + path TEXT NOT NULL, + branch_name TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'Active', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + merged_at TEXT, + merged_into TEXT +); + +CREATE TABLE IF NOT EXISTS notifications ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + ticket_id TEXT REFERENCES processed_tickets(id), + type TEXT NOT NULL, + title TEXT NOT NULL, + message TEXT NOT NULL, + read INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs new file mode 100644 index 0000000..d61f705 --- /dev/null +++ b/src-tauri/src/db.rs @@ -0,0 +1,85 @@ +use rusqlite::{Connection, Result}; +use std::path::Path; + +const MIGRATION_001: &str = include_str!("../migrations/001_init.sql"); + +pub fn init(db_path: &Path) -> Result { + let conn = Connection::open(db_path)?; + configure(&conn)?; + migrate(&conn)?; + Ok(conn) +} + +pub fn init_in_memory() -> Result { + let conn = Connection::open_in_memory()?; + configure(&conn)?; + migrate(&conn)?; + Ok(conn) +} + +fn configure(conn: &Connection) -> Result<()> { + conn.pragma_update(None, "journal_mode", "wal")?; + conn.pragma_update(None, "foreign_keys", "ON")?; + Ok(()) +} + +fn migrate(conn: &Connection) -> Result<()> { + let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; + + if version < 1 { + conn.execute_batch(MIGRATION_001)?; + conn.pragma_update(None, "user_version", 1)?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_init_in_memory_creates_tables() { + let conn = init_in_memory().expect("should initialize"); + + // Verify all 6 tables exist + let tables: Vec = conn + .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name") + .unwrap() + .query_map([], |row| row.get(0)) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert_eq!( + tables, + vec![ + "notifications", + "processed_tickets", + "projects", + "tuleap_credentials", + "watched_trackers", + "worktrees", + ] + ); + } + + #[test] + fn test_init_in_memory_enables_foreign_keys() { + let conn = init_in_memory().expect("should initialize"); + let fk_enabled: i32 = conn + .query_row("PRAGMA foreign_keys", [], |row| row.get(0)) + .unwrap(); + assert_eq!(fk_enabled, 1); + } + + #[test] + fn test_migration_is_idempotent() { + let conn = init_in_memory().expect("should initialize"); + // Running init again on same connection should not fail + let version: i32 = conn + .pragma_query_value(None, "user_version", |row| row.get(0)) + .unwrap(); + assert_eq!(version, 1); + } +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs new file mode 100644 index 0000000..7b33c2f --- /dev/null +++ b/src-tauri/src/error.rs @@ -0,0 +1,34 @@ +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct AppError { + pub message: String, +} + +impl From for AppError { + fn from(e: rusqlite::Error) -> Self { + AppError { + message: e.to_string(), + } + } +} + +impl From for AppError { + fn from(e: std::io::Error) -> Self { + AppError { + message: e.to_string(), + } + } +} + +impl From for AppError { + fn from(s: String) -> Self { + AppError { message: s } + } +} + +impl std::fmt::Display for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 41900e7..db8ceca 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,26 @@ +mod db; +mod error; + +use std::sync::Mutex; +use tauri::Manager; + +pub struct AppState { + pub db: Mutex, +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() - .invoke_handler(tauri::generate_handler![]) + .setup(|app| { + let db_dir = app.path().app_data_dir()?; + std::fs::create_dir_all(&db_dir)?; + let db_path = db_dir.join("orchai.db"); + let conn = db::init(&db_path).expect("Failed to initialize database"); + app.manage(AppState { + db: Mutex::new(conn), + }); + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }