Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!

"$schema" = "../../schemas/schema.json"

[[permission]]
identifier = "allow-remove-update-hook"
description = "Enables the remove_update_hook command without any pre-configured scope."
commands.allow = ["remove_update_hook"]

[[permission]]
identifier = "deny-remove-update-hook"
description = "Denies the remove_update_hook command without any pre-configured scope."
commands.deny = ["remove_update_hook"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!

"$schema" = "../../schemas/schema.json"

[[permission]]
identifier = "allow-setup-update-hook"
description = "Enables the setup_update_hook command without any pre-configured scope."
commands.allow = ["setup_update_hook"]

[[permission]]
identifier = "deny-setup-update-hook"
description = "Denies the setup_update_hook command without any pre-configured scope."
commands.deny = ["setup_update_hook"]
52 changes: 52 additions & 0 deletions plugins/sql/permissions/autogenerated/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,32 @@ Denies the load command without any pre-configured scope.
<tr>
<td>

`sql:allow-remove-update-hook`

</td>
<td>

Enables the remove_update_hook command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`sql:deny-remove-update-hook`

</td>
<td>

Denies the remove_update_hook command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`sql:allow-select`

</td>
Expand All @@ -126,6 +152,32 @@ Enables the select command without any pre-configured scope.

Denies the select command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`sql:allow-setup-update-hook`

</td>
<td>

Enables the setup_update_hook command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`sql:deny-setup-update-hook`

</td>
<td>

Denies the setup_update_hook command without any pre-configured scope.

</td>
</tr>
</table>
24 changes: 24 additions & 0 deletions plugins/sql/permissions/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,18 @@
"const": "deny-load",
"markdownDescription": "Denies the load command without any pre-configured scope."
},
{
"description": "Enables the remove_update_hook command without any pre-configured scope.",
"type": "string",
"const": "allow-remove-update-hook",
"markdownDescription": "Enables the remove_update_hook command without any pre-configured scope."
},
{
"description": "Denies the remove_update_hook command without any pre-configured scope.",
"type": "string",
"const": "deny-remove-update-hook",
"markdownDescription": "Denies the remove_update_hook command without any pre-configured scope."
},
{
"description": "Enables the select command without any pre-configured scope.",
"type": "string",
Expand All @@ -342,6 +354,18 @@
"const": "deny-select",
"markdownDescription": "Denies the select command without any pre-configured scope."
},
{
"description": "Enables the setup_update_hook command without any pre-configured scope.",
"type": "string",
"const": "allow-setup-update-hook",
"markdownDescription": "Enables the setup_update_hook command without any pre-configured scope."
},
{
"description": "Denies the setup_update_hook command without any pre-configured scope.",
"type": "string",
"const": "deny-setup-update-hook",
"markdownDescription": "Denies the setup_update_hook command without any pre-configured scope."
},
{
"description": "### Default Permissions\n\nThis permission set configures what kind of\ndatabase operations are available from the sql plugin.\n\n### Granted Permissions\n\nAll reading related operations are enabled.\nAlso allows to load or close a connection.\n\n\n#### This default permission set includes:\n\n- `allow-close`\n- `allow-load`\n- `allow-select`",
"type": "string",
Expand Down
8 changes: 7 additions & 1 deletion plugins/sql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
mod commands;
mod decode;
mod error;
#[cfg(feature = "sqlite")]
mod update_hook;
mod wrapper;

pub use error::Error;
Expand Down Expand Up @@ -140,7 +142,11 @@ impl Builder {
commands::load,
commands::execute,
commands::select,
commands::close
commands::close,
#[cfg(feature = "sqlite")]
update_hook::setup_update_hook,
#[cfg(feature = "sqlite")]
update_hook::remove_update_hook,
])
.setup(|app, api| {
let config = api.config().clone().unwrap_or_default();
Expand Down
98 changes: 98 additions & 0 deletions plugins/sql/src/update_hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use serde::Serialize;
use tauri::{command, AppHandle, Emitter, Runtime, State};

use crate::{DbInstances, DbPool, Error};

#[derive(Clone, Serialize)]
pub struct UpdateHookEvent {
pub operation: String,
pub database: String,
pub table: String,
pub rowid: i64,
}

#[command]
pub(crate) async fn setup_update_hook<R: Runtime>(
app: AppHandle<R>,
db_instances: State<'_, DbInstances>,
db: String,
) -> Result<(), Error> {
#[cfg(feature = "sqlite")]
{
let instances = db_instances.0.read().await;
let db_pool = instances
.get(&db)
.ok_or_else(|| Error::DatabaseNotLoaded(db.clone()))?;

let sqlite_pool = match db_pool {
DbPool::Sqlite(pool) => pool,
_ => return Err(Error::InvalidDbUrl(
format!("Cannot setup update hook for {}: update hooks are only supported for SQLite databases", db)
)),
};

sqlite_pool
.rebuild_pool(Some(move |result: sqlx::sqlite::UpdateHookResult| {
let operation = match result.operation {
sqlx::sqlite::SqliteOperation::Insert => "INSERT",
sqlx::sqlite::SqliteOperation::Update => "UPDATE",
sqlx::sqlite::SqliteOperation::Delete => "DELETE",
sqlx::sqlite::SqliteOperation::Unknown(_) => "UNKNOWN",
};

let event = UpdateHookEvent {
operation: operation.to_string(),
database: result.database.to_string(),
table: result.table.to_string(),
rowid: result.rowid,
};

if let Err(e) = app.emit("sqlite-update-hook", &event) {
log::error!("[tauri-plugin-sql] Failed to emit update hook event: {}", e);
}
}))
.await
.map_err(Error::Sql)?;

Ok(())
}
}

#[command]
pub(crate) async fn remove_update_hook(
db_instances: State<'_, DbInstances>,
db: String,
) -> Result<(), Error> {
#[cfg(feature = "sqlite")]
{
let instances = db_instances.0.read().await;
let db_pool = instances
.get(&db)
.ok_or_else(|| Error::DatabaseNotLoaded(db.clone()))?;

let sqlite_pool = match db_pool {
DbPool::Sqlite(pool) => pool,
_ => return Err(Error::InvalidDbUrl(
format!("Cannot remove update hook for {}: update hooks are only supported for SQLite databases", db)
)),
};

sqlite_pool
.rebuild_pool(None::<fn(sqlx::sqlite::UpdateHookResult)>)
.await
.map_err(Error::Sql)?;

Ok(())
}

#[cfg(not(feature = "sqlite"))]
{
Err(Error::InvalidDbUrl(
"Update hooks are only supported for SQLite".to_string(),
))
}
}
73 changes: 66 additions & 7 deletions plugins/sql/src/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,63 @@ use sqlx::MySql;
#[cfg(feature = "postgres")]
use sqlx::Postgres;
#[cfg(feature = "sqlite")]
use sqlx::Sqlite;
use sqlx::{pool::PoolOptions, sqlite::SqliteConnection, Sqlite};
#[cfg(feature = "sqlite")]
use std::sync::Arc;
#[cfg(feature = "sqlite")]
use tokio::sync::RwLock;

use crate::LastInsertId;

#[cfg(feature = "sqlite")]
pub struct SqlitePoolWithHook {
pool: Arc<RwLock<Pool<Sqlite>>>,
db_url: String,
}

#[cfg(feature = "sqlite")]
impl SqlitePoolWithHook {
pub fn pool(&self) -> Arc<RwLock<Pool<Sqlite>>> {
Arc::clone(&self.pool)
}

pub fn db_url(&self) -> &str {
&self.db_url
}

pub async fn rebuild_pool<F>(&self, hook_fn: Option<F>) -> Result<(), sqlx::Error>
where
F: Fn(sqlx::sqlite::UpdateHookResult) + Send + Sync + 'static,
{
let new_pool = if let Some(hook_fn) = hook_fn {
let hook_fn = Arc::new(hook_fn);
PoolOptions::new()
.after_connect(move |conn: &mut SqliteConnection, _meta| {
let hook_fn = Arc::clone(&hook_fn);
Box::pin(async move {
conn.lock_handle().await?.set_update_hook(move |result| {
hook_fn(result);
});
Ok(())
})
})
.connect(&self.db_url)
.await?
} else {
Pool::connect(&self.db_url).await?
};

let mut pool_guard = self.pool.write().await;
pool_guard.close().await;
*pool_guard = new_pool;

Ok(())
}
}

pub enum DbPool {
#[cfg(feature = "sqlite")]
Sqlite(Pool<Sqlite>),
Sqlite(SqlitePoolWithHook),
#[cfg(feature = "mysql")]
MySql(Pool<MySql>),
#[cfg(feature = "postgres")]
Expand Down Expand Up @@ -88,7 +138,11 @@ impl DbPool {
if !Sqlite::database_exists(conn_url).await.unwrap_or(false) {
Sqlite::create_database(conn_url).await?;
}
Ok(Self::Sqlite(Pool::connect(conn_url).await?))
let pool = Pool::connect(conn_url).await?;
Ok(Self::Sqlite(SqlitePoolWithHook {
pool: Arc::new(RwLock::new(pool)),
db_url: conn_url.to_string(),
}))
}
#[cfg(feature = "mysql")]
"mysql" => {
Expand Down Expand Up @@ -119,7 +173,10 @@ impl DbPool {
) -> Result<(), crate::Error> {
match self {
#[cfg(feature = "sqlite")]
DbPool::Sqlite(pool) => _migrator.run(pool).await?,
DbPool::Sqlite(sqlite_pool) => {
let pool = sqlite_pool.pool.read().await;
_migrator.run(&*pool).await?
}
#[cfg(feature = "mysql")]
DbPool::MySql(pool) => _migrator.run(pool).await?,
#[cfg(feature = "postgres")]
Expand All @@ -133,7 +190,7 @@ impl DbPool {
pub(crate) async fn close(&self) {
match self {
#[cfg(feature = "sqlite")]
DbPool::Sqlite(pool) => pool.close().await,
DbPool::Sqlite(sqlite_pool) => sqlite_pool.pool.read().await.close().await,
#[cfg(feature = "mysql")]
DbPool::MySql(pool) => pool.close().await,
#[cfg(feature = "postgres")]
Expand All @@ -150,7 +207,8 @@ impl DbPool {
) -> Result<(u64, LastInsertId), crate::Error> {
Ok(match self {
#[cfg(feature = "sqlite")]
DbPool::Sqlite(pool) => {
DbPool::Sqlite(sqlite_pool) => {
let pool = sqlite_pool.pool.read().await;
let mut query = sqlx::query(&_query);
for value in _values {
if value.is_null() {
Expand Down Expand Up @@ -218,7 +276,8 @@ impl DbPool {
) -> Result<Vec<IndexMap<String, JsonValue>>, crate::Error> {
Ok(match self {
#[cfg(feature = "sqlite")]
DbPool::Sqlite(pool) => {
DbPool::Sqlite(sqlite_pool) => {
let pool = sqlite_pool.pool.read().await;
let mut query = sqlx::query(&_query);
for value in _values {
if value.is_null() {
Expand Down
Loading