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
|
||||
} 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 @@
|
||||
<Inspect name="devStoreExtCmds" value={$devStoreExtCmds} />
|
||||
<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
|
||||
class={cn("h-screen rounded-lg shadow-md")}
|
||||
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 { 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 */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
@ -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"
|
||||
|
@ -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<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>> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
<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`
|
||||
|
||||
</td>
|
||||
@ -1610,6 +1636,32 @@ Denies the search_extension_data command without any pre-configured scope.
|
||||
<tr>
|
||||
<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`
|
||||
|
||||
</td>
|
||||
|
@ -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",
|
||||
|
@ -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<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_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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user