diff --git a/Cargo.lock b/Cargo.lock index b0f3cac1..fbd3846a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6920,6 +6920,7 @@ version = "2.2.0" dependencies = [ "futures-core", "indexmap 2.7.0", + "libsqlite3-sys", "log", "serde", "serde_json", diff --git a/plugins/sql/.vscode/settings.json b/plugins/sql/.vscode/settings.json new file mode 100644 index 00000000..b12ccd3d --- /dev/null +++ b/plugins/sql/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.cargo.features": ["mysql", "postgres", "sqlite"] +} diff --git a/plugins/sql/Cargo.toml b/plugins/sql/Cargo.toml index 0449a4be..5cd27a85 100644 --- a/plugins/sql/Cargo.toml +++ b/plugins/sql/Cargo.toml @@ -36,8 +36,9 @@ time = "0.3" tokio = { version = "1", features = ["sync"] } indexmap = { version = "2", features = ["serde"] } +libsqlite3-sys = { version = "0.30.1", features = ["bundled-sqlcipher"] } + [features] sqlite = ["sqlx/sqlite", "sqlx/runtime-tokio"] mysql = ["sqlx/mysql", "sqlx/runtime-tokio-rustls"] postgres = ["sqlx/postgres", "sqlx/runtime-tokio-rustls"] -# TODO: bundled-cipher etc diff --git a/plugins/sql/api-iife.js b/plugins/sql/api-iife.js index a30f68d9..86daa539 100644 --- a/plugins/sql/api-iife.js +++ b/plugins/sql/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_SQL__=function(){"use strict";async function e(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}"function"==typeof SuppressedError&&SuppressedError;class t{constructor(e){this.path=e}static async load(s){const n=await e("plugin:sql|load",{db:s});return new t(n)}static get(e){return new t(e)}async execute(t,s){const[n,r]=await e("plugin:sql|execute",{db:this.path,query:t,values:s??[]});return{lastInsertId:r,rowsAffected:n}}async select(t,s){return await e("plugin:sql|select",{db:this.path,query:t,values:s??[]})}async close(t){return await e("plugin:sql|close",{db:t})}}return t}();Object.defineProperty(window.__TAURI__,"sql",{value:__TAURI_PLUGIN_SQL__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_SQL__=function(){"use strict";async function t(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}"function"==typeof SuppressedError&&SuppressedError;class e{constructor(t){this.path=t}static async load(s,n){const r=await t("plugin:sql|load",{db:s,options:n});return new e(r)}static get(t){return new e(t)}async execute(e,s){const[n,r]=await t("plugin:sql|execute",{db:this.path,query:e,values:s??[]});return{lastInsertId:r,rowsAffected:n}}async select(e,s){return await t("plugin:sql|select",{db:this.path,query:e,values:s??[]})}async close(e){return await t("plugin:sql|close",{db:e})}}return e}();Object.defineProperty(window.__TAURI__,"sql",{value:__TAURI_PLUGIN_SQL__})} diff --git a/plugins/sql/guest-js/index.ts b/plugins/sql/guest-js/index.ts index 11d39e70..51f7c014 100644 --- a/plugins/sql/guest-js/index.ts +++ b/plugins/sql/guest-js/index.ts @@ -42,12 +42,31 @@ export default class Database { * * @example * ```ts + * // Basic connection to a SQLite database * const db = await Database.load("sqlite:test.db"); + * + * // Connecting with an encryption key for added security + * const db = await Database.load("sqlite:encrypted.db", { + * sqlite: { pragmas: { "key": "encryption_key" } } + * }); + * + * // Connecting with specific pragmas for configuration + * const db = await Database.load("sqlite:test.db", { + * sqlite: { pragmas: { "journal_mode": "WAL", "foreign_keys": "ON" } } + * }); * ``` */ - static async load(path: string): Promise { + static async load( + path: string, + options?: { + sqlite?: { + pragmas?: Record + } + } + ): Promise { const _path = await invoke('plugin:sql|load', { - db: path + db: path, + options }) return new Database(_path) diff --git a/plugins/sql/src/commands.rs b/plugins/sql/src/commands.rs index 760d00b2..e599e423 100644 --- a/plugins/sql/src/commands.rs +++ b/plugins/sql/src/commands.rs @@ -2,22 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use crate::{wrapper::ConnectionOptions, DbInstances, DbPool, Error, LastInsertId, Migrations}; use indexmap::IndexMap; use serde_json::Value as JsonValue; use sqlx::migrate::Migrator; use tauri::{command, AppHandle, Runtime, State}; -use crate::{DbInstances, DbPool, Error, LastInsertId, Migrations}; - #[command] pub(crate) async fn load( app: AppHandle, db_instances: State<'_, DbInstances>, migrations: State<'_, Migrations>, db: String, + options: Option, ) -> Result { - let pool = DbPool::connect(&db, &app).await?; - + let pool = DbPool::connect(&db, &app, options).await?; if let Some(migrations) = migrations.0.lock().await.remove(&db) { let migrator = Migrator::new(migrations).await?; pool.migrate(&migrator).await?; diff --git a/plugins/sql/src/lib.rs b/plugins/sql/src/lib.rs index 56b2a3a6..64d6cf1c 100644 --- a/plugins/sql/src/lib.rs +++ b/plugins/sql/src/lib.rs @@ -16,6 +16,9 @@ mod wrapper; pub use error::Error; pub use wrapper::DbPool; +#[cfg(feature = "sqlite")] +pub use wrapper::SqliteOptions; +pub use wrapper::ConnectionOptions; use futures_core::future::BoxFuture; use serde::{Deserialize, Serialize}; @@ -150,7 +153,7 @@ impl Builder { let mut lock = instances.0.write().await; for db in config.preload { - let pool = DbPool::connect(&db, app).await?; + let pool = DbPool::connect(&db, app, None).await?; if let Some(migrations) = self.migrations.as_mut().and_then(|mm| mm.remove(&db)) diff --git a/plugins/sql/src/wrapper.rs b/plugins/sql/src/wrapper.rs index d47b2d1c..cd146764 100644 --- a/plugins/sql/src/wrapper.rs +++ b/plugins/sql/src/wrapper.rs @@ -2,11 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#[cfg(feature = "sqlite")] +use std::collections::HashMap; #[cfg(feature = "sqlite")] use std::fs::create_dir_all; use indexmap::IndexMap; use serde_json::Value as JsonValue; +#[cfg(feature = "sqlite")] +use sqlx::sqlite::SqliteConnectOptions; #[cfg(any(feature = "sqlite", feature = "mysql", feature = "postgres"))] use sqlx::{migrate::MigrateDatabase, Column, Executor, Pool, Row}; #[cfg(any(feature = "sqlite", feature = "mysql", feature = "postgres"))] @@ -33,6 +37,40 @@ pub enum DbPool { None, } +#[cfg(feature = "sqlite")] +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct SqliteOptions { + pub pragmas: HashMap, +} + +#[cfg(feature = "sqlite")] +impl Default for SqliteOptions { + fn default() -> Self { + Self { + pragmas: HashMap::new(), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct ConnectionOptions { + #[cfg(feature = "sqlite")] + pub sqlite: Option, + // #[cfg(feature = "mysql")] + // mysql: Option, + // #[cfg(feature = "postgres")] + // postgres: Option, +} + +impl Default for ConnectionOptions { + fn default() -> Self { + Self { + #[cfg(feature = "sqlite")] + sqlite: None, + } + } +} + // public methods /* impl DbPool { /// Get the inner Sqlite Pool. Returns None for MySql and Postgres pools. @@ -68,6 +106,7 @@ impl DbPool { pub(crate) async fn connect( conn_url: &str, _app: &AppHandle, + options: Option, ) -> Result { match conn_url .split_once(':') @@ -82,13 +121,23 @@ impl DbPool { .expect("No App config path was found!"); create_dir_all(&app_path).expect("Couldn't create app config dir"); - let conn_url = &path_mapper(app_path, conn_url); + let filename = conn_url.split_once(':').unwrap().1; - if !Sqlite::database_exists(conn_url).await.unwrap_or(false) { - Sqlite::create_database(conn_url).await?; + let mut sqlite_options = SqliteConnectOptions::new() + .filename(filename) + .create_if_missing(true); + // Apply pragmas if provided + if let Some(conn_opts) = options { + if let Some(sqlite_opts) = conn_opts.sqlite { + for (pragma_name, pragma_value) in sqlite_opts.pragmas { + sqlite_options = sqlite_options.pragma(pragma_name, pragma_value); + } + } } - Ok(Self::Sqlite(Pool::connect(conn_url).await?)) + + // Connect with options (which includes create_if_missing) + Ok(Self::Sqlite(Pool::connect_with(sqlite_options).await?)) } #[cfg(feature = "mysql")] "mysql" => {