mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-18 06:50:51 +01:00
refatored vault
This commit is contained in:
@ -1,48 +1,374 @@
|
||||
mod permissions;
|
||||
// src-tauri/src/extension/database/mod.rs
|
||||
|
||||
use crate::database;
|
||||
use crate::database::DbConnection;
|
||||
use crate::models::ExtensionState;
|
||||
pub mod permissions;
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::AppState;
|
||||
use permissions::{check_read_permission, check_write_permission, PermissionError};
|
||||
use rusqlite::params_from_iter;
|
||||
use rusqlite::types::Value as SqlValue;
|
||||
use rusqlite::Transaction;
|
||||
use serde_json::json;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use tauri::{AppHandle, State};
|
||||
// Extension-bezogene Funktionen mit extension_-Präfix
|
||||
/// Lädt eine Extension aus einer Manifest-Datei
|
||||
/* #[tauri::command]
|
||||
pub fn extension_load(
|
||||
manifest_path: String,
|
||||
app: AppHandle,
|
||||
) -> Result<crate::models::ExtensionManifest, String> {
|
||||
let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())?;
|
||||
let manifest: crate::models::ExtensionManifest =
|
||||
serde_json::from_str(&manifest_content).map_err(|e| e.to_string())?;
|
||||
app.state::<ExtensionState>()
|
||||
.add_extension(manifest_path.clone(), manifest.clone());
|
||||
Ok(manifest)
|
||||
}
|
||||
*/
|
||||
/// Führt SQL-Leseoperationen mit Berechtigungsprüfung aus
|
||||
#[tauri::command]
|
||||
pub async fn extension_sql_select(
|
||||
app: AppHandle,
|
||||
extension_id: String,
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
state: State<'_, DbConnection>,
|
||||
) -> Result<Vec<Vec<JsonValue>>, String> {
|
||||
permissions::check_read_permission(&app, &extension_id, &sql).await?;
|
||||
database::core::select(sql, params, &state).await
|
||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||
use std::collections::HashSet;
|
||||
use tauri::State;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Combined error type für Extension-Database operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionDatabaseError {
|
||||
#[error("Permission denied: {source}")]
|
||||
Permission {
|
||||
#[from]
|
||||
source: PermissionError,
|
||||
},
|
||||
#[error("Database error: {source}")]
|
||||
Database {
|
||||
#[from]
|
||||
source: DatabaseError,
|
||||
},
|
||||
#[error("Parameter validation failed: {reason}")]
|
||||
ParameterValidation { reason: String },
|
||||
#[error("Statement execution failed: {reason}")]
|
||||
StatementExecution { reason: String },
|
||||
}
|
||||
|
||||
// Für Tauri Command Serialization
|
||||
impl serde::Serialize for ExtensionDatabaseError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt Statements mit korrekter Parameter-Bindung aus
|
||||
pub struct StatementExecutor<'a> {
|
||||
transaction: &'a Transaction<'a>,
|
||||
hlc_service: &'a HlcService,
|
||||
}
|
||||
|
||||
impl<'a> StatementExecutor<'a> {
|
||||
fn new(transaction: &'a Transaction<'a>, hlc_service: &'a HlcService) -> Self {
|
||||
Self {
|
||||
transaction,
|
||||
hlc_service,
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt ein einzelnes Statement mit Parametern aus
|
||||
fn execute_statement_with_params(
|
||||
&self,
|
||||
statement: &Statement,
|
||||
params: &[SqlValue],
|
||||
) -> Result<(), ExtensionDatabaseError> {
|
||||
let sql = statement.to_string();
|
||||
let expected_params = count_sql_placeholders(&sql);
|
||||
|
||||
if expected_params != params.len() {
|
||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
||||
reason: format!(
|
||||
"Parameter count mismatch for statement: {} (expected: {}, provided: {})",
|
||||
truncate_sql(&sql, 100),
|
||||
expected_params,
|
||||
params.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
self.transaction
|
||||
.execute(&sql, params_from_iter(params.iter()))
|
||||
.map_err(|e| ExtensionDatabaseError::StatementExecution {
|
||||
reason: format!(
|
||||
"Failed to execute statement on table {}: {}",
|
||||
self.extract_table_name_from_statement(statement)
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
e
|
||||
),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extrahiert den Tabellennamen aus einem Statement für bessere Fehlermeldungen
|
||||
fn extract_table_name_from_statement(&self, statement: &Statement) -> Option<String> {
|
||||
match statement {
|
||||
Statement::Insert(insert) => {
|
||||
if let TableObject::TableName(name) = &insert.table {
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Statement::Update { table, .. } => {
|
||||
if let TableFactor::Table { name, .. } = &table.relation {
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Statement::Delete(delete) => {
|
||||
// Verbessertes Extrahieren für DELETE
|
||||
use sqlparser::ast::FromTable;
|
||||
match &delete.from {
|
||||
FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => {
|
||||
if !tables.is_empty() {
|
||||
if let TableFactor::Table { name, .. } = &tables[0].relation {
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if !delete.tables.is_empty() {
|
||||
Some(delete.tables[0].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Statement::CreateTable(create) => Some(create.name.to_string()),
|
||||
Statement::AlterTable { name, .. } => Some(name.to_string()),
|
||||
Statement::Drop { names, .. } => names.first().map(|name| name.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt SQL-Schreiboperationen mit Berechtigungsprüfung aus
|
||||
#[tauri::command]
|
||||
pub async fn extension_sql_execute(
|
||||
app: AppHandle,
|
||||
extension_id: String,
|
||||
sql: String,
|
||||
sql: &str,
|
||||
params: Vec<JsonValue>,
|
||||
state: State<'_, DbConnection>,
|
||||
) -> Result<usize, String> {
|
||||
permissions::check_write_permission(&app, &extension_id, &sql).await?;
|
||||
database::core::execute(sql, params, &state).await
|
||||
extension_id: String,
|
||||
state: State<'_, AppState>,
|
||||
hlc_service: State<'_, HlcService>,
|
||||
) -> Result<Vec<String>, ExtensionDatabaseError> {
|
||||
// Permission check
|
||||
check_write_permission(&state.db, &extension_id, sql).await?;
|
||||
|
||||
// Parameter validation
|
||||
validate_params(sql, ¶ms)?;
|
||||
|
||||
// SQL parsing
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
|
||||
// Database operation
|
||||
with_connection(&state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let transformer = CrdtTransformer::new();
|
||||
let executor = StatementExecutor::new(&tx, &hlc_service);
|
||||
|
||||
// Generate HLC timestamp
|
||||
let hlc_timestamp =
|
||||
hlc_service
|
||||
.new_timestamp_and_persist(&tx)
|
||||
.map_err(|e| DatabaseError::HlcError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
// Transform statements
|
||||
let mut modified_schema_tables = HashSet::new();
|
||||
for statement in &mut ast_vec {
|
||||
if let Some(table_name) =
|
||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
||||
{
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert parameters
|
||||
let sql_values = ValueConverter::convert_params(¶ms)?;
|
||||
|
||||
// Execute statements
|
||||
for statement in ast_vec {
|
||||
executor.execute_statement_with_params(&statement, &sql_values)?;
|
||||
|
||||
if let Statement::CreateTable(create_table_details) = statement {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
println!(
|
||||
"Table '{}' created by extension, setting up CRDT triggers...",
|
||||
table_name_str
|
||||
);
|
||||
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
|
||||
println!(
|
||||
"Triggers for table '{}' successfully created.",
|
||||
table_name_str
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
|
||||
Ok(modified_schema_tables.into_iter().collect())
|
||||
})
|
||||
.map_err(ExtensionDatabaseError::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn extension_sql_select(
|
||||
sql: &str,
|
||||
params: Vec<JsonValue>,
|
||||
extension_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<JsonValue>, ExtensionDatabaseError> {
|
||||
// Permission check
|
||||
check_read_permission(&state.db, &extension_id, sql).await?;
|
||||
|
||||
// Parameter validation
|
||||
validate_params(sql, ¶ms)?;
|
||||
|
||||
// SQL parsing
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
|
||||
if ast_vec.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// Validate that all statements are queries
|
||||
for stmt in &ast_vec {
|
||||
if !matches!(stmt, Statement::Query(_)) {
|
||||
return Err(ExtensionDatabaseError::StatementExecution {
|
||||
reason: "Only SELECT statements are allowed in extension_sql_select".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Database operation
|
||||
with_connection(&state.db, |conn| {
|
||||
let sql_params = ValueConverter::convert_params(¶ms)?;
|
||||
let transformer = CrdtTransformer::new();
|
||||
|
||||
// Use the last statement for result set
|
||||
let last_statement = ast_vec.pop().unwrap();
|
||||
let mut stmt_to_execute = last_statement;
|
||||
|
||||
// Transform the statement
|
||||
transformer.transform_select_statement(&mut stmt_to_execute)?;
|
||||
let transformed_sql = stmt_to_execute.to_string();
|
||||
|
||||
// Prepare and execute query
|
||||
let mut prepared_stmt =
|
||||
conn.prepare(&transformed_sql)
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: transformed_sql.clone(),
|
||||
reason: e.to_string(),
|
||||
table: None,
|
||||
})?;
|
||||
|
||||
let column_names: Vec<String> = prepared_stmt
|
||||
.column_names()
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let rows = prepared_stmt
|
||||
.query_map(params_from_iter(sql_params.iter()), |row| {
|
||||
row_to_json_value(row, &column_names)
|
||||
})
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row_result in rows {
|
||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: e.to_string(),
|
||||
})?);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
})
|
||||
.map_err(ExtensionDatabaseError::from)
|
||||
}
|
||||
|
||||
/// Konvertiert eine SQLite-Zeile zu JSON
|
||||
fn row_to_json_value(
|
||||
row: &rusqlite::Row,
|
||||
columns: &[String],
|
||||
) -> Result<JsonValue, rusqlite::Error> {
|
||||
let mut map = serde_json::Map::new();
|
||||
for (i, col_name) in columns.iter().enumerate() {
|
||||
let value = row.get::<usize, rusqlite::types::Value>(i)?;
|
||||
let json_value = match value {
|
||||
rusqlite::types::Value::Null => JsonValue::Null,
|
||||
rusqlite::types::Value::Integer(i) => json!(i),
|
||||
rusqlite::types::Value::Real(f) => json!(f),
|
||||
rusqlite::types::Value::Text(s) => json!(s),
|
||||
rusqlite::types::Value::Blob(blob) => json!(blob.to_vec()),
|
||||
};
|
||||
map.insert(col_name.clone(), json_value);
|
||||
}
|
||||
Ok(JsonValue::Object(map))
|
||||
}
|
||||
|
||||
/// Validiert Parameter gegen SQL-Platzhalter
|
||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), ExtensionDatabaseError> {
|
||||
let total_placeholders = count_sql_placeholders(sql);
|
||||
|
||||
if total_placeholders != params.len() {
|
||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
||||
reason: format!(
|
||||
"Parameter count mismatch: SQL has {} placeholders but {} parameters provided",
|
||||
total_placeholders,
|
||||
params.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Zählt SQL-Platzhalter (verbesserte Version)
|
||||
fn count_sql_placeholders(sql: &str) -> usize {
|
||||
sql.matches('?').count()
|
||||
}
|
||||
|
||||
/// Kürzt SQL für Fehlermeldungen
|
||||
fn truncate_sql(sql: &str, max_length: usize) -> String {
|
||||
if sql.len() <= max_length {
|
||||
sql.to_string()
|
||||
} else {
|
||||
format!("{}...", &sql[..max_length])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_count_sql_placeholders() {
|
||||
assert_eq!(
|
||||
count_sql_placeholders("SELECT * FROM users WHERE id = ?"),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
count_sql_placeholders("SELECT * FROM users WHERE id = ? AND name = ?"),
|
||||
2
|
||||
);
|
||||
assert_eq!(count_sql_placeholders("SELECT * FROM users"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_sql() {
|
||||
let sql = "SELECT * FROM very_long_table_name";
|
||||
assert_eq!(truncate_sql(sql, 10), "SELECT * F...");
|
||||
assert_eq!(truncate_sql(sql, 50), sql);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_params() {
|
||||
let params = vec![json!(1), json!("test")];
|
||||
|
||||
assert!(validate_params("SELECT * FROM users WHERE id = ? AND name = ?", ¶ms).is_ok());
|
||||
assert!(validate_params("SELECT * FROM users WHERE id = ?", ¶ms).is_err());
|
||||
assert!(validate_params("SELECT * FROM users", ¶ms).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user