mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-18 23:07:27 +02:00
feat(sql): add autoCreate option to Database.load (#3702)
* feat(sql): add autoCreate option to Database.load Add utoCreate option to Database.load allowing users to disable implicit database creation when loading a database. Closes #299 * fix(sql): fix unused variable without a database feature * refactor(sql): share the database existence check between drivers * fix(sql): don't report a failed existence check as a missing database * fix(sql): name the database as passed, without password, in the missing database error * fix(sql): don't create the app config dir when autoCreate is false * refactor(sql): use the LoadOptions default for missing options * test(sql): cover autoCreate and password redaction --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
1 parent
0374bec1e2
commit
c91a9e6af5
7 files changed
+151
-19
No files matched your search
@@ -1 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_SQL__=function(e){"use strict";class t{constructor(e){this.path=e}static async load(n){const s=await e.invoke("plugin:sql|load",{db:n});return new t(s)}static get(e){return new t(e)}async execute(t,n){const[s,a]=await e.invoke("plugin:sql|execute",{db:this.path,query:t,values:n??[]});return{lastInsertId:a,rowsAffected:s}}async select(t,n){return await e.invoke("plugin:sql|select",{db:this.path,query:t,values:n??[]})}async close(t){return await e.invoke("plugin:sql|close",{db:t})}}return t}(window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"sql",{value:__TAURI_PLUGIN_SQL__})}
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_SQL__=function(e){"use strict";class t{constructor(e){this.path=e}static async load(n,s){const i=await e.invoke("plugin:sql|load",{db:n,options:s});return new t(i)}static get(e){return new t(e)}async execute(t,n){const[s,i]=await e.invoke("plugin:sql|execute",{db:this.path,query:t,values:n??[]});return{lastInsertId:i,rowsAffected:s}}async select(t,n){return await e.invoke("plugin:sql|select",{db:this.path,query:t,values:n??[]})}async close(t){return await e.invoke("plugin:sql|close",{db:t})}}return t}(window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"sql",{value:__TAURI_PLUGIN_SQL__})}
|
||||
@@ -29,6 +29,19 @@ export interface QueryResult {
|
||||
lastInsertId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for loading a database.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*/
|
||||
export interface LoadOptions {
|
||||
/**
|
||||
* Whether to automatically create the database if it does not exist yet.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
autoCreate?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* **Database**
|
||||
*
|
||||
@@ -76,14 +89,17 @@ export default class Database {
|
||||
* ```typescript
|
||||
* import Database from '@tauri-apps/plugin-sql'
|
||||
* const db = await Database.load('sqlite:test.db')
|
||||
* const existingDb = await Database.load('sqlite:test.db', { autoCreate: false })
|
||||
* ```
|
||||
*
|
||||
* @param path The database connection string, such as `sqlite:test.db`. The database is created if it does not exist yet, and any migration registered for it on the Rust side is run.
|
||||
* @param path The database connection string, such as `sqlite:test.db`. The database is created if it does not exist yet (unless `options.autoCreate` is `false`), and any migration registered for it on the Rust side is run.
|
||||
* @param options Configuration options for connecting to the database.
|
||||
* @returns A promise resolving to a `Database` instance connected to the given database.
|
||||
*/
|
||||
static async load(path: string): Promise<Database> {
|
||||
static async load(path: string, options?: LoadOptions): Promise<Database> {
|
||||
const _path = await invoke<string>('plugin:sql|load', {
|
||||
db: path
|
||||
db: path,
|
||||
options
|
||||
})
|
||||
|
||||
return new Database(_path)
|
||||
|
||||
@@ -9,14 +9,28 @@ use tauri::{AppHandle, Runtime, State, command};
|
||||
|
||||
use crate::{DbInstances, DbPool, Error, LastInsertId, Migrations};
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub(crate) struct LoadOptions {
|
||||
pub auto_create: bool,
|
||||
}
|
||||
|
||||
impl Default for LoadOptions {
|
||||
fn default() -> Self {
|
||||
Self { auto_create: true }
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn load<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
db_instances: State<'_, DbInstances>,
|
||||
migrations: State<'_, Migrations>,
|
||||
db: String,
|
||||
options: Option<LoadOptions>,
|
||||
) -> Result<String, crate::Error> {
|
||||
let pool = DbPool::connect(&db, &app).await?;
|
||||
let options = options.unwrap_or_default();
|
||||
let pool = DbPool::connect(&db, &app, options.auto_create).await?;
|
||||
|
||||
if let Some(migrations) = migrations.0.lock().await.remove(&db) {
|
||||
let migrator = Migrator::new(migrations).await?;
|
||||
|
||||
@@ -26,6 +26,10 @@ pub enum Error {
|
||||
/// Contains the connection string of the database.
|
||||
#[error("database {0} not loaded")]
|
||||
DatabaseNotLoaded(String),
|
||||
/// The database does not exist and `autoCreate` was disabled when loading it.
|
||||
/// Contains the connection string of the database.
|
||||
#[error("database {0} does not exist")]
|
||||
DatabaseDoesNotExist(String),
|
||||
/// A value selected from the database has a SQL type that cannot be converted
|
||||
/// to JSON. Contains the name of that SQL type.
|
||||
#[error("unsupported datatype: {0}")]
|
||||
|
||||
@@ -217,7 +217,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, true).await?;
|
||||
|
||||
if let Some(migrations) =
|
||||
self.migrations.as_mut().and_then(|mm| mm.remove(&db))
|
||||
|
||||
+105
-13
@@ -78,6 +78,7 @@ impl DbPool {
|
||||
pub(crate) async fn connect<R: Runtime>(
|
||||
conn_url: &str,
|
||||
_app: &AppHandle<R>,
|
||||
_auto_create: bool,
|
||||
) -> Result<Self, crate::Error> {
|
||||
match conn_url
|
||||
.split_once(':')
|
||||
@@ -91,27 +92,25 @@ impl DbPool {
|
||||
.app_config_dir()
|
||||
.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);
|
||||
|
||||
if !Sqlite::database_exists(conn_url).await.unwrap_or(false) {
|
||||
Sqlite::create_database(conn_url).await?;
|
||||
if _auto_create {
|
||||
create_dir_all(&app_path).expect("Couldn't create app config dir");
|
||||
}
|
||||
Ok(Self::Sqlite(Pool::connect(conn_url).await?))
|
||||
|
||||
let db_url = &path_mapper(app_path, conn_url);
|
||||
|
||||
ensure_database::<Sqlite>(db_url, conn_url, _auto_create).await?;
|
||||
Ok(Self::Sqlite(Pool::connect(db_url).await?))
|
||||
}
|
||||
#[cfg(feature = "mysql")]
|
||||
"mysql" => {
|
||||
if !MySql::database_exists(conn_url).await.unwrap_or(false) {
|
||||
MySql::create_database(conn_url).await?;
|
||||
}
|
||||
ensure_database::<MySql>(conn_url, &redact_password(conn_url), _auto_create)
|
||||
.await?;
|
||||
Ok(Self::MySql(Pool::connect(conn_url).await?))
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
"postgres" => {
|
||||
if !Postgres::database_exists(conn_url).await.unwrap_or(false) {
|
||||
Postgres::create_database(conn_url).await?;
|
||||
}
|
||||
ensure_database::<Postgres>(conn_url, &redact_password(conn_url), _auto_create)
|
||||
.await?;
|
||||
Ok(Self::Postgres(Pool::connect(conn_url).await?))
|
||||
}
|
||||
#[cfg(not(any(feature = "sqlite", feature = "postgres", feature = "mysql")))]
|
||||
@@ -323,6 +322,43 @@ impl DbPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the database if it does not exist yet, or fails with
|
||||
/// [`crate::Error::DatabaseDoesNotExist`] (naming the connection string `db`
|
||||
/// the app passed) when `auto_create` is `false`.
|
||||
/// Errors of the existence check itself (unreachable server, bad credentials)
|
||||
/// are returned as they are, not as a missing database.
|
||||
#[cfg(any(feature = "sqlite", feature = "mysql", feature = "postgres"))]
|
||||
async fn ensure_database<DB: MigrateDatabase>(
|
||||
conn_url: &str,
|
||||
db: &str,
|
||||
auto_create: bool,
|
||||
) -> Result<(), crate::Error> {
|
||||
match DB::database_exists(conn_url).await {
|
||||
Ok(true) => Ok(()),
|
||||
// When the check fails, creating reports the actual error (or fixes it).
|
||||
Ok(false) | Err(_) if auto_create => Ok(DB::create_database(conn_url).await?),
|
||||
Ok(false) => Err(crate::Error::DatabaseDoesNotExist(db.to_string())),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the password from a `scheme://user:password@host/db` connection string,
|
||||
/// so it can be put in an error message.
|
||||
#[cfg(any(feature = "mysql", feature = "postgres"))]
|
||||
fn redact_password(conn_url: &str) -> String {
|
||||
let Some((scheme, rest)) = conn_url.split_once("://") else {
|
||||
return conn_url.to_string();
|
||||
};
|
||||
let authority_end = rest.find(['/', '?']).unwrap_or(rest.len());
|
||||
match rest[..authority_end].rsplit_once('@') {
|
||||
Some((userinfo, _)) => match userinfo.split_once(':') {
|
||||
Some((user, _)) => format!("{scheme}://{user}:***@{}", &rest[userinfo.len() + 1..]),
|
||||
None => conn_url.to_string(),
|
||||
},
|
||||
None => conn_url.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
/// Maps the user supplied DB connection string to a connection string
|
||||
/// with a fully qualified file path to the App's designed "app_path"
|
||||
@@ -341,3 +377,59 @@ fn path_mapper(mut app_path: std::path::PathBuf, connection_string: &str) -> Str
|
||||
.expect("Problem creating fully qualified path to Database file!")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[test]
|
||||
fn ensure_database_respects_auto_create() {
|
||||
use super::ensure_database;
|
||||
use sqlx::Sqlite;
|
||||
|
||||
let path = std::env::temp_dir().join(format!("tauri-plugin-sql-{}.db", std::process::id()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let url = format!("sqlite:{}", path.display());
|
||||
|
||||
tauri::async_runtime::block_on(async {
|
||||
let err = ensure_database::<Sqlite>(&url, "sqlite:test.db", false)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.to_string(), "database sqlite:test.db does not exist");
|
||||
assert!(!path.exists());
|
||||
|
||||
ensure_database::<Sqlite>(&url, "sqlite:test.db", true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(path.exists());
|
||||
|
||||
ensure_database::<Sqlite>(&url, "sqlite:test.db", false)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mysql", feature = "postgres"))]
|
||||
#[test]
|
||||
fn redact_password() {
|
||||
use super::redact_password;
|
||||
|
||||
assert_eq!(
|
||||
redact_password("mysql://user:secret@localhost:3306/db"),
|
||||
"mysql://user:***@localhost:3306/db"
|
||||
);
|
||||
assert_eq!(
|
||||
redact_password("postgres://user:p@ss@host/db?sslmode=require"),
|
||||
"postgres://user:***@host/db?sslmode=require"
|
||||
);
|
||||
assert_eq!(
|
||||
redact_password("postgres://user@host/db"),
|
||||
"postgres://user@host/db"
|
||||
);
|
||||
assert_eq!(
|
||||
redact_password("postgres://host/db?user=a@b"),
|
||||
"postgres://host/db?user=a@b"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user