diff --git a/apps/desktop/src/routes/app/+page.svelte b/apps/desktop/src/routes/app/+page.svelte index 50bf56a..cb978bb 100644 --- a/apps/desktop/src/routes/app/+page.svelte +++ b/apps/desktop/src/routes/app/+page.svelte @@ -31,6 +31,7 @@ SystemCmds } from "@kksh/ui/main" import { cn } from "@kksh/ui/utils" + import * as db from "@kunkunapi/src/commands/db" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWindow, Window } from "@tauri-apps/api/window" import { platform } from "@tauri-apps/plugin-os" @@ -110,6 +111,22 @@ --> + + (generateJarvisPluginCommand("select"), { + query, + values + }) +} + +export function execute(query: string, values: any[]) { + return invoke(generateJarvisPluginCommand("execute"), { + query, + values + }) +} + /* -------------------------------------------------------------------------- */ /* Extension CRUD */ /* -------------------------------------------------------------------------- */ diff --git a/packages/db/Cargo.toml b/packages/db/Cargo.toml index e7d9e9b..74b86c6 100644 --- a/packages/db/Cargo.toml +++ b/packages/db/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -rusqlite = { version = "0.31.0", features = ["bundled-sqlcipher-vendored-openssl"] } +rusqlite = { version = "0.31.0", features = [ + "bundled-sqlcipher-vendored-openssl", +] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tempfile = "3.10.1" diff --git a/packages/db/src/lib.rs b/packages/db/src/lib.rs index 5f4d27c..c5d3580 100644 --- a/packages/db/src/lib.rs +++ b/packages/db/src/lib.rs @@ -2,6 +2,7 @@ pub mod models; pub mod schema; use models::{CmdType, ExtDataField, ExtDataSearchQuery, SearchMode}; use rusqlite::{params, params_from_iter, Connection, Result, ToSql}; +use serde_json::Value as JsonValue; use serde_json::{json, Value}; use std::path::Path; @@ -122,6 +123,95 @@ impl JarvisDB { Ok(()) } + pub fn select(&self, query: String, values: Vec) -> Result> { + let mut stmt = self.conn.prepare(&query)?; + + // Convert JsonValue parameters to appropriate types for rusqlite + let mut params: Vec> = Vec::new(); + for value in values { + if value.is_null() { + params.push(Box::new(Option::::None)); + } else if value.is_string() { + params.push(Box::new(value.as_str().unwrap().to_owned())); + } else if let Some(number) = value.as_number() { + if number.is_i64() { + params.push(Box::new(number.as_i64().unwrap())); + } else if number.is_u64() { + params.push(Box::new(number.as_u64().unwrap() as i64)); + } else { + params.push(Box::new(number.as_f64().unwrap())); + } + } else { + params.push(Box::new(value.to_string())); + } + } + + // Get column names from statement + let column_names: Vec = (0..stmt.column_count()) + .map(|i| stmt.column_name(i).unwrap().to_string()) + .collect(); + + // Execute the query with the converted parameters and map results + let rows = stmt.query_map(params_from_iter(params.iter().map(|p| p.as_ref())), |row| { + let mut result = serde_json::Map::new(); + for (i, name) in column_names.iter().enumerate() { + let value: Value = match row.get_ref(i)? { + rusqlite::types::ValueRef::Null => Value::Null, + rusqlite::types::ValueRef::Integer(i) => Value::Number(i.into()), + rusqlite::types::ValueRef::Real(r) => Value::Number( + serde_json::Number::from_f64(r).unwrap_or(serde_json::Number::from(0)), + ), + rusqlite::types::ValueRef::Text(t) => { + Value::String(String::from_utf8_lossy(t).into_owned()) + } + rusqlite::types::ValueRef::Blob(b) => { + Value::String(String::from_utf8_lossy(b).into_owned()) + } + }; + result.insert(name.clone(), value); + } + Ok(Value::Object(result)) + })?; + + let mut results = Vec::new(); + for row in rows { + results.push(row?); + } + Ok(results) + } + + pub fn execute(&self, query: &str, values: Vec) -> Result<(u64, i64)> { + let mut stmt = self.conn.prepare(query)?; + + // Convert JsonValue parameters to appropriate types for rusqlite + let mut params: Vec> = Vec::new(); + for value in values { + if value.is_null() { + params.push(Box::new(Option::::None)); + } else if value.is_string() { + params.push(Box::new(value.as_str().unwrap().to_owned())); + } else if let Some(number) = value.as_number() { + if number.is_i64() { + params.push(Box::new(number.as_i64().unwrap())); + } else if number.is_u64() { + params.push(Box::new(number.as_u64().unwrap() as i64)); + } else { + params.push(Box::new(number.as_f64().unwrap())); + } + } else { + params.push(Box::new(value.to_string())); + } + } + + // Execute the query with the converted parameters + let rows_affected = stmt.execute(params_from_iter(params.iter().map(|p| p.as_ref())))?; + + // Get the last insert rowid + let last_insert_id = self.conn.last_insert_rowid(); + + Ok((rows_affected as u64, last_insert_id)) + } + pub fn get_all_extensions(&self) -> Result> { let mut stmt = self.conn.prepare( "SELECT ext_id, identifier, path, data, version, enabled, installed_at FROM extensions", @@ -983,4 +1073,72 @@ mod tests { fs::remove_file(&db_path).unwrap(); } + + #[test] + fn test_select_and_execute() { + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test.db"); + let db = JarvisDB::new(&db_path, None).unwrap(); + db.init().unwrap(); + + // Create a simple todo table + db.execute( + "CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT, completed BOOLEAN)", + vec![], + ) + .unwrap(); + + // Test execute with INSERT + let (rows_affected, last_id) = db + .execute( + "INSERT INTO todos (title, completed) VALUES (?1, ?2)", + vec![json!("Buy groceries"), json!(false)], + ) + .unwrap(); + assert_eq!(rows_affected, 1); + assert_eq!(last_id, 1); + + // Test select with basic query + let results = db + .select( + "SELECT title, completed FROM todos WHERE id = ?1".to_string(), + vec![json!(1)], + ) + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0]["title"], "Buy groceries"); + assert_eq!(results[0]["completed"], "false"); + + // Test execute with UPDATE + let (rows_affected, _) = db + .execute( + "UPDATE todos SET completed = ?1 WHERE id = ?2", + vec![json!(true), json!(1)], + ) + .unwrap(); + assert_eq!(rows_affected, 1); + + // Verify the update with select + let results = db + .select( + "SELECT completed FROM todos WHERE id = ?1".to_string(), + vec![json!(1)], + ) + .unwrap(); + assert_eq!(results[0]["completed"], "true"); + + // Test execute with DELETE + let (rows_affected, _) = db + .execute("DELETE FROM todos WHERE id = ?1", vec![json!(1)]) + .unwrap(); + assert_eq!(rows_affected, 1); + + // Verify the deletion + let results = db + .select("SELECT COUNT(*) as count FROM todos".to_string(), vec![]) + .unwrap(); + assert_eq!(results[0]["count"], 0); + + fs::remove_file(&db_path).unwrap(); + } } diff --git a/packages/tauri-plugins/jarvis/build.rs b/packages/tauri-plugins/jarvis/build.rs index f3bcbc4..94829c3 100644 --- a/packages/tauri-plugins/jarvis/build.rs +++ b/packages/tauri-plugins/jarvis/build.rs @@ -98,6 +98,8 @@ const COMMANDS: &[&str] = &[ "search_extension_data", "delete_extension_data_by_id", "update_extension_data_by_id", + "select", + "execute", /* -------------------------------- Clipboard ------------------------------- */ "add_to_history", "get_history", diff --git a/packages/tauri-plugins/jarvis/permissions/all.toml b/packages/tauri-plugins/jarvis/permissions/all.toml index 3e5222d..77fa122 100644 --- a/packages/tauri-plugins/jarvis/permissions/all.toml +++ b/packages/tauri-plugins/jarvis/permissions/all.toml @@ -65,6 +65,8 @@ commands.allow = [ "get_ext_label_map", "get_frontmost_app", # Database + "select", + "execute", "create_extension", "get_all_extensions", "get_unique_extension_by_identifier", diff --git a/packages/tauri-plugins/jarvis/permissions/autogenerated/commands/execute.toml b/packages/tauri-plugins/jarvis/permissions/autogenerated/commands/execute.toml new file mode 100644 index 0000000..d98be89 --- /dev/null +++ b/packages/tauri-plugins/jarvis/permissions/autogenerated/commands/execute.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-execute" +description = "Enables the execute command without any pre-configured scope." +commands.allow = ["execute"] + +[[permission]] +identifier = "deny-execute" +description = "Denies the execute command without any pre-configured scope." +commands.deny = ["execute"] diff --git a/packages/tauri-plugins/jarvis/permissions/autogenerated/commands/select.toml b/packages/tauri-plugins/jarvis/permissions/autogenerated/commands/select.toml new file mode 100644 index 0000000..5a13a02 --- /dev/null +++ b/packages/tauri-plugins/jarvis/permissions/autogenerated/commands/select.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-select" +description = "Enables the select command without any pre-configured scope." +commands.allow = ["select"] + +[[permission]] +identifier = "deny-select" +description = "Denies the select command without any pre-configured scope." +commands.deny = ["select"] diff --git a/packages/tauri-plugins/jarvis/permissions/autogenerated/reference.md b/packages/tauri-plugins/jarvis/permissions/autogenerated/reference.md index 43a78fd..06546b4 100644 --- a/packages/tauri-plugins/jarvis/permissions/autogenerated/reference.md +++ b/packages/tauri-plugins/jarvis/permissions/autogenerated/reference.md @@ -492,6 +492,32 @@ Denies the empty_trash command without any pre-configured scope. +`jarvis:allow-execute` + + + + +Enables the execute command without any pre-configured scope. + + + + + + + +`jarvis:deny-execute` + + + + +Denies the execute command without any pre-configured scope. + + + + + + + `jarvis:allow-file-search` @@ -1610,6 +1636,32 @@ Denies the search_extension_data command without any pre-configured scope. +`jarvis:allow-select` + + + + +Enables the select command without any pre-configured scope. + + + + + + + +`jarvis:deny-select` + + + + +Denies the select command without any pre-configured scope. + + + + + + + `jarvis:allow-server-is-running` diff --git a/packages/tauri-plugins/jarvis/permissions/schemas/schema.json b/packages/tauri-plugins/jarvis/permissions/schemas/schema.json index 32302ff..ac9ff84 100644 --- a/packages/tauri-plugins/jarvis/permissions/schemas/schema.json +++ b/packages/tauri-plugins/jarvis/permissions/schemas/schema.json @@ -479,6 +479,16 @@ "type": "string", "const": "deny-empty-trash" }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "allow-execute" + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "deny-execute" + }, { "description": "Enables the file_search command without any pre-configured scope.", "type": "string", @@ -909,6 +919,16 @@ "type": "string", "const": "deny-search-extension-data" }, + { + "description": "Enables the select command without any pre-configured scope.", + "type": "string", + "const": "allow-select" + }, + { + "description": "Denies the select command without any pre-configured scope.", + "type": "string", + "const": "deny-select" + }, { "description": "Enables the server_is_running command without any pre-configured scope.", "type": "string", diff --git a/packages/tauri-plugins/jarvis/src/commands/db.rs b/packages/tauri-plugins/jarvis/src/commands/db.rs index f833837..dc0a1ff 100644 --- a/packages/tauri-plugins/jarvis/src/commands/db.rs +++ b/packages/tauri-plugins/jarvis/src/commands/db.rs @@ -2,6 +2,7 @@ use db::{ models::{Cmd, CmdType, Ext, ExtData, ExtDataField, ExtDataSearchQuery, SQLSortOrder}, JarvisDB, }; +use serde_json::{json, Value as JsonValue}; use std::{path::PathBuf, sync::Mutex}; use tauri::State; @@ -245,3 +246,31 @@ pub async fn update_extension_data_by_id( .update_extension_data_by_id(data_id, data, search_text) .map_err(|err| err.to_string()) } + +#[tauri::command] +pub async fn select( + db: State<'_, DBState>, + query: &str, + values: Vec, +) -> Result, String> { + db.db + .lock() + .unwrap() + .select(query.to_string(), values) + .map_err(|err| err.to_string()) +} + +#[tauri::command] +pub async fn execute( + db: State<'_, DBState>, + query: &str, + values: Vec, +) -> Result, String> { + let (rows_affected, last_id) = db + .db + .lock() + .unwrap() + .execute(query, values) + .map_err(|err| err.to_string())?; + Ok(vec![json!([rows_affected, last_id])]) +} diff --git a/packages/tauri-plugins/jarvis/src/lib.rs b/packages/tauri-plugins/jarvis/src/lib.rs index 58da91d..06d3087 100644 --- a/packages/tauri-plugins/jarvis/src/lib.rs +++ b/packages/tauri-plugins/jarvis/src/lib.rs @@ -140,6 +140,9 @@ pub fn init() -> TauriPlugin { // commands::storage::ext_store_wrapper_load, // commands::storage::ext_store_wrapper_save, /* -------------------------------- database -------------------------------- */ + commands::db::select, + commands::db::execute, + commands::db::create_extension, commands::db::create_extension, commands::db::get_all_extensions, commands::db::get_unique_extension_by_identifier,