mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-04-20 05:29:17 +00:00
feat: add database select and execute commands
- Introduced `select` and `execute` functions in the database module to facilitate querying and executing SQL commands. - Updated the Tauri plugin to expose these commands, allowing for database interactions from the frontend. - Added corresponding permissions for the new commands in the permissions configuration. - Enhanced the database library with JSON value handling for query parameters.
This commit is contained in:
parent
b3e2082abe
commit
3271507d0c
@ -31,6 +31,7 @@
|
|||||||
SystemCmds
|
SystemCmds
|
||||||
} from "@kksh/ui/main"
|
} from "@kksh/ui/main"
|
||||||
import { cn } from "@kksh/ui/utils"
|
import { cn } from "@kksh/ui/utils"
|
||||||
|
import * as db from "@kunkunapi/src/commands/db"
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
import { getCurrentWindow, Window } from "@tauri-apps/api/window"
|
import { getCurrentWindow, Window } from "@tauri-apps/api/window"
|
||||||
import { platform } from "@tauri-apps/plugin-os"
|
import { platform } from "@tauri-apps/plugin-os"
|
||||||
@ -110,6 +111,22 @@
|
|||||||
<Inspect name="devStoreExtCmds" value={$devStoreExtCmds} />
|
<Inspect name="devStoreExtCmds" value={$devStoreExtCmds} />
|
||||||
<Inspect name="$appState.searchTerm" value={$appState.searchTerm} />
|
<Inspect name="$appState.searchTerm" value={$appState.searchTerm} />
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onclick={() => {
|
||||||
|
db.select("SELECT * FROM extensions;", []).then((res) => {
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO extension_data (ext_id, data_type, data, search_text, metadata) VALUES (?, ?, ?, ?, ?);",
|
||||||
|
[1, "Test", "Hello, world!", "Hello, world!", "{'metadata': 'test'}"]
|
||||||
|
).then((res) => {
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
<Command.Root
|
<Command.Root
|
||||||
class={cn("h-screen rounded-lg shadow-md")}
|
class={cn("h-screen rounded-lg shadow-md")}
|
||||||
bind:value={$appState.highlightedCmd}
|
bind:value={$appState.highlightedCmd}
|
||||||
|
@ -5,6 +5,34 @@ import { CmdType, Ext, ExtCmd, ExtData } from "../models/extension"
|
|||||||
import { convertDateToSqliteString, SearchMode, SearchModeEnum, SQLSortOrder } from "../models/sql"
|
import { convertDateToSqliteString, SearchMode, SearchModeEnum, SQLSortOrder } from "../models/sql"
|
||||||
import { generateJarvisPluginCommand } from "./common"
|
import { generateJarvisPluginCommand } from "./common"
|
||||||
|
|
||||||
|
export interface QueryResult {
|
||||||
|
/** The number of rows affected by the query. */
|
||||||
|
rowsAffected: number
|
||||||
|
/**
|
||||||
|
* The last inserted `id`.
|
||||||
|
*
|
||||||
|
* This value is not set for Postgres databases. If the
|
||||||
|
* last inserted id is required on Postgres, the `select` function
|
||||||
|
* must be used, with a `RETURNING` clause
|
||||||
|
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
|
||||||
|
*/
|
||||||
|
lastInsertId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function select(query: string, values: any[]) {
|
||||||
|
return invoke<any[]>(generateJarvisPluginCommand("select"), {
|
||||||
|
query,
|
||||||
|
values
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function execute(query: string, values: any[]) {
|
||||||
|
return invoke<QueryResult>(generateJarvisPluginCommand("execute"), {
|
||||||
|
query,
|
||||||
|
values
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Extension CRUD */
|
/* Extension CRUD */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
@ -4,7 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[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 = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
|
@ -2,6 +2,7 @@ pub mod models;
|
|||||||
pub mod schema;
|
pub mod schema;
|
||||||
use models::{CmdType, ExtDataField, ExtDataSearchQuery, SearchMode};
|
use models::{CmdType, ExtDataField, ExtDataSearchQuery, SearchMode};
|
||||||
use rusqlite::{params, params_from_iter, Connection, Result, ToSql};
|
use rusqlite::{params, params_from_iter, Connection, Result, ToSql};
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@ -122,6 +123,95 @@ impl JarvisDB {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select(&self, query: String, values: Vec<JsonValue>) -> Result<Vec<JsonValue>> {
|
||||||
|
let mut stmt = self.conn.prepare(&query)?;
|
||||||
|
|
||||||
|
// Convert JsonValue parameters to appropriate types for rusqlite
|
||||||
|
let mut params: Vec<Box<dyn ToSql>> = Vec::new();
|
||||||
|
for value in values {
|
||||||
|
if value.is_null() {
|
||||||
|
params.push(Box::new(Option::<String>::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<String> = (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<JsonValue>) -> Result<(u64, i64)> {
|
||||||
|
let mut stmt = self.conn.prepare(query)?;
|
||||||
|
|
||||||
|
// Convert JsonValue parameters to appropriate types for rusqlite
|
||||||
|
let mut params: Vec<Box<dyn ToSql>> = Vec::new();
|
||||||
|
for value in values {
|
||||||
|
if value.is_null() {
|
||||||
|
params.push(Box::new(Option::<String>::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<Vec<models::Ext>> {
|
pub fn get_all_extensions(&self) -> Result<Vec<models::Ext>> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT ext_id, identifier, path, data, version, enabled, installed_at FROM extensions",
|
"SELECT ext_id, identifier, path, data, version, enabled, installed_at FROM extensions",
|
||||||
@ -983,4 +1073,72 @@ mod tests {
|
|||||||
|
|
||||||
fs::remove_file(&db_path).unwrap();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,8 @@ const COMMANDS: &[&str] = &[
|
|||||||
"search_extension_data",
|
"search_extension_data",
|
||||||
"delete_extension_data_by_id",
|
"delete_extension_data_by_id",
|
||||||
"update_extension_data_by_id",
|
"update_extension_data_by_id",
|
||||||
|
"select",
|
||||||
|
"execute",
|
||||||
/* -------------------------------- Clipboard ------------------------------- */
|
/* -------------------------------- Clipboard ------------------------------- */
|
||||||
"add_to_history",
|
"add_to_history",
|
||||||
"get_history",
|
"get_history",
|
||||||
|
@ -65,6 +65,8 @@ commands.allow = [
|
|||||||
"get_ext_label_map",
|
"get_ext_label_map",
|
||||||
"get_frontmost_app",
|
"get_frontmost_app",
|
||||||
# Database
|
# Database
|
||||||
|
"select",
|
||||||
|
"execute",
|
||||||
"create_extension",
|
"create_extension",
|
||||||
"get_all_extensions",
|
"get_all_extensions",
|
||||||
"get_unique_extension_by_identifier",
|
"get_unique_extension_by_identifier",
|
||||||
|
@ -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"]
|
@ -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"]
|
@ -492,6 +492,32 @@ Denies the empty_trash command without any pre-configured scope.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
`jarvis:allow-execute`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the execute command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`jarvis:deny-execute`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the execute command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
`jarvis:allow-file-search`
|
`jarvis:allow-file-search`
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
@ -1610,6 +1636,32 @@ Denies the search_extension_data command without any pre-configured scope.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
`jarvis:allow-select`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Enables the select command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
`jarvis:deny-select`
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
Denies the select command without any pre-configured scope.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
`jarvis:allow-server-is-running`
|
`jarvis:allow-server-is-running`
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
@ -479,6 +479,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-empty-trash"
|
"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.",
|
"description": "Enables the file_search command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -909,6 +919,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "deny-search-extension-data"
|
"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.",
|
"description": "Enables the server_is_running command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -2,6 +2,7 @@ use db::{
|
|||||||
models::{Cmd, CmdType, Ext, ExtData, ExtDataField, ExtDataSearchQuery, SQLSortOrder},
|
models::{Cmd, CmdType, Ext, ExtData, ExtDataField, ExtDataSearchQuery, SQLSortOrder},
|
||||||
JarvisDB,
|
JarvisDB,
|
||||||
};
|
};
|
||||||
|
use serde_json::{json, Value as JsonValue};
|
||||||
use std::{path::PathBuf, sync::Mutex};
|
use std::{path::PathBuf, sync::Mutex};
|
||||||
use tauri::State;
|
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)
|
.update_extension_data_by_id(data_id, data, search_text)
|
||||||
.map_err(|err| err.to_string())
|
.map_err(|err| err.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn select(
|
||||||
|
db: State<'_, DBState>,
|
||||||
|
query: &str,
|
||||||
|
values: Vec<JsonValue>,
|
||||||
|
) -> Result<Vec<JsonValue>, 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<JsonValue>,
|
||||||
|
) -> Result<Vec<JsonValue>, 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])])
|
||||||
|
}
|
||||||
|
@ -140,6 +140,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
// commands::storage::ext_store_wrapper_load,
|
// commands::storage::ext_store_wrapper_load,
|
||||||
// commands::storage::ext_store_wrapper_save,
|
// commands::storage::ext_store_wrapper_save,
|
||||||
/* -------------------------------- database -------------------------------- */
|
/* -------------------------------- database -------------------------------- */
|
||||||
|
commands::db::select,
|
||||||
|
commands::db::execute,
|
||||||
|
commands::db::create_extension,
|
||||||
commands::db::create_extension,
|
commands::db::create_extension,
|
||||||
commands::db::get_all_extensions,
|
commands::db::get_all_extensions,
|
||||||
commands::db::get_unique_extension_by_identifier,
|
commands::db::get_unique_extension_by_identifier,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user