Skip to content
Merged
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
822 changes: 789 additions & 33 deletions apps/tauri/src-tauri/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/tauri/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ipnetwork = "0.20"
mac_address = "1.1"
bit-vec = "0.6.3"
redis = { version = "0.27", features = ["aio", "tokio-comp"] }
mongodb = "3.10.1"

[profile.release]
codegen-units = 1
Expand Down
203 changes: 201 additions & 2 deletions apps/tauri/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::connection::{
ConnectionConfig, ConnectionError, ConnectionId, ConnectionManager, ConnectionRegistry,
ErdSchema, PostgresObjectDetail, RedisKeyMeta, RedisKeyView, RedisScanPage, SchemaObject,
SchemaObjectKind, SqlObjectKind, SqlObjectNode, SqliteOverview, TableSchema,
ErdSchema, MongoFindResult, PostgresObjectDetail, RedisKeyMeta, RedisKeyView, RedisScanPage,
SchemaObject, SchemaObjectKind, SqlObjectKind, SqlObjectNode, SqliteOverview, TableSchema,
};
use crate::profiles::{ConnectionProfile, ProfileStore, ProfileStoreError};
use crate::secrets::SharedSecretStore;
Expand Down Expand Up @@ -41,6 +41,21 @@ pub struct ConnectRedisRequest {
pub id: Option<String>,
}

/// Mongo save+connect args.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConnectMongoRequest {
pub name: String,
pub host: String,
pub port: u16,
pub database: String,
pub username: Option<String>,
pub password: Option<String>,
pub auth_source: Option<String>,
pub folder_id: Option<String>,
pub id: Option<String>,
}

#[tauri::command]
pub fn db_connect_sqlite(
path: String,
Expand Down Expand Up @@ -110,6 +125,72 @@ pub fn db_connect_redis(
})
}

#[tauri::command]
pub fn db_connect_mongo(
host: String,
port: u16,
database: String,
username: Option<String>,
password: Option<String>,
auth_source: Option<String>,
id: Option<String>,
registry: State<'_, ConnectionRegistry>,
) -> Result<ConnectionId, ConnectionError> {
registry.connect(ConnectionConfig::Mongo {
host,
port,
database,
username,
password,
auth_source,
id,
})
}

#[tauri::command]
pub fn mongo_ping(
id: ConnectionId,
registry: State<'_, ConnectionRegistry>,
) -> Result<String, ConnectionError> {
registry.with_mongo(&id, |s| s.ping())
}

#[tauri::command]
pub fn mongo_list_databases(
id: ConnectionId,
registry: State<'_, ConnectionRegistry>,
) -> Result<Vec<String>, ConnectionError> {
registry.with_mongo(&id, |s| s.list_databases())
}

#[tauri::command]
pub fn mongo_list_collections(
id: ConnectionId,
database: String,
registry: State<'_, ConnectionRegistry>,
) -> Result<Vec<String>, ConnectionError> {
registry.with_mongo(&id, |s| s.list_collections(&database))
}

#[tauri::command]
pub fn mongo_find(
id: ConnectionId,
database: String,
collection: String,
filter_json: Option<String>,
limit: Option<i64>,
registry: State<'_, ConnectionRegistry>,
) -> Result<MongoFindResult, ConnectionError> {
registry.with_mongo(&id, |s| {
s.find(
&database,
&collection,
filter_json.as_deref(),
limit,
)
})
}

#[tauri::command]
pub fn redis_ping(
id: ConnectionId,
Expand Down Expand Up @@ -562,6 +643,124 @@ pub fn profiles_connect_redis(
Ok(profile)
}

/// Save Mongo profile metadata (no password) and open a live session.
#[tauri::command]
pub fn profiles_connect_mongo(
request: ConnectMongoRequest,
store: State<'_, ProfileStore>,
secrets: State<'_, SharedSecretStore>,
registry: State<'_, ConnectionRegistry>,
) -> Result<ConnectionProfile, ConnectionError> {
let ConnectMongoRequest {
name,
host,
port,
database,
username,
password,
auth_source,
folder_id,
id,
} = request;

let database = {
let t = database.trim();
if t.is_empty() {
"querya".to_owned()
} else {
t.to_owned()
}
};
let user = username
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let auth = auth_source
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned);

let display_name = {
let t = name.trim();
if t.is_empty() {
format!("{host}:{port}/{database}")
} else {
t.to_owned()
}
};

let mut profile = match id.filter(|s| !s.trim().is_empty()) {
Some(existing_id) => store
.get(&existing_id)
.map_err(|e| ConnectionError::io(e.to_string()))?
.map(|mut p| {
p.name = display_name.clone();
p.host = Some(host.clone());
p.port = Some(i64::from(port));
p.database_name = Some(database.clone());
p.username = user.clone();
p.folder_id = folder_id.clone();
p.driver = "mongo".into();
p.path = None;
p
})
.unwrap_or_else(|| {
let mut p = ConnectionProfile::new_mongo(
display_name.clone(),
host.clone(),
port,
database.clone(),
user.clone(),
);
p.id = existing_id;
p.folder_id = folder_id.clone();
p
}),
None => {
let mut p = ConnectionProfile::new_mongo(
display_name,
host.clone(),
port,
database.clone(),
user.clone(),
);
p.folder_id = folder_id;
p
}
};
profile.touch();
store
.upsert(&profile)
.map_err(|e| ConnectionError::io(e.to_string()))?;

let password = match password {
Some(p) => {
secrets
.set_password(&profile.id, &p)
.map_err(|e| ConnectionError::io(format!("keyring write: {e}")))?;
Some(p)
}
None => secrets
.get_password(&profile.id)
.map_err(|e| ConnectionError::io(format!("keyring read: {e}")))?,
};

if !registry.is_connected(&ConnectionId::from(profile.id.as_str())) {
registry.connect(ConnectionConfig::Mongo {
host,
port,
database,
username: user,
password,
auth_source: auth,
id: Some(profile.id.clone()),
})?;
}
Ok(profile)
}

#[tauri::command]
pub fn db_disconnect(
id: ConnectionId,
Expand Down
4 changes: 3 additions & 1 deletion apps/tauri/src-tauri/src/connection/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ impl From<&str> for ConnectionId {
}
}

/// Driver family wired (or stubbed) in this crate. Mongo lands with its epic.
/// Driver family wired (or stubbed) in this crate.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DriverKind {
Sqlite,
Postgres,
Mysql,
Redis,
Mongo,
}

impl DriverKind {
Expand All @@ -58,6 +59,7 @@ impl DriverKind {
Self::Postgres => "postgres",
Self::Mysql => "mysql",
Self::Redis => "redis",
Self::Mongo => "mongo",
}
}
}
Expand Down
64 changes: 61 additions & 3 deletions apps/tauri/src-tauri/src/connection/manager.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::error::{ConnectionError, ConnectionResult};
use super::id::{ConnectionId, DriverKind};
use super::mongo::MongoSession;
use super::mysql::MysqlSession;
use super::postgres::PostgresSession;
use super::query::QueryEvent;
Expand Down Expand Up @@ -55,6 +56,19 @@ pub enum ConnectionConfig {
#[serde(default)]
id: Option<String>,
},
Mongo {
host: String,
port: u16,
database: String,
#[serde(default)]
username: Option<String>,
#[serde(default)]
password: Option<String>,
#[serde(default)]
auth_source: Option<String>,
#[serde(default)]
id: Option<String>,
},
}

impl ConnectionConfig {
Expand All @@ -64,6 +78,7 @@ impl ConnectionConfig {
Self::Postgres { .. } => DriverKind::Postgres,
Self::Mysql { .. } => DriverKind::Mysql,
Self::Redis { .. } => DriverKind::Redis,
Self::Mongo { .. } => DriverKind::Mongo,
}
}
}
Expand Down Expand Up @@ -105,7 +120,7 @@ impl ConnectionRegistry {
Self::with_builtin_drivers()
}

/// Registry with SQLite + Postgres + MySQL + Redis openers.
/// Registry with SQLite + Postgres + MySQL + Redis + Mongo openers.
pub fn with_builtin_drivers() -> Self {
Self::with_opener(|config| match config {
ConnectionConfig::Sqlite { path, id } => {
Expand Down Expand Up @@ -171,6 +186,26 @@ impl ConnectionRegistry {
})?;
Ok(Arc::new(session) as SessionArc)
}
ConnectionConfig::Mongo {
host,
port,
database,
username,
password,
auth_source,
id,
} => {
let session = super::mongo::open_mongo(super::mongo::MongoConnectParams {
host,
port,
database,
username,
password,
auth_source,
id: id.filter(|s| !s.trim().is_empty()).map(ConnectionId::from),
})?;
Ok(Arc::new(session) as SessionArc)
}
})
}

Expand Down Expand Up @@ -272,6 +307,21 @@ impl ConnectionRegistry {
f(redis)
}

pub fn with_mongo<T>(
&self,
id: &ConnectionId,
f: impl FnOnce(&MongoSession) -> ConnectionResult<T>,
) -> ConnectionResult<T> {
let session = self.session_arc(id)?;
let mongo = session
.as_any()
.downcast_ref::<MongoSession>()
.ok_or(ConnectionError::UnsupportedDriver {
driver: session.driver(),
})?;
f(mongo)
}

/// Driver-aware query stream (SQLite + Postgres + MySQL).
///
/// `confirmed` must be true for DROP / TRUNCATE / unconditional DELETE|UPDATE
Expand All @@ -288,7 +338,7 @@ impl ConnectionRegistry {
let driver = self
.driver_of(id)
.ok_or_else(|| ConnectionError::NotFound { id: id.clone() })?;
if matches!(driver, DriverKind::Redis) {
if matches!(driver, DriverKind::Redis | DriverKind::Mongo) {
return Err(ConnectionError::UnsupportedDriver { driver });
}
super::sql_guard::assert_query_allowed(sql, driver, confirmed)?;
Expand All @@ -298,7 +348,9 @@ impl ConnectionRegistry {
self.with_postgres(id, |s| s.execute_streaming(sql, limit, emit))
}
DriverKind::Mysql => self.with_mysql(id, |s| s.execute_streaming(sql, limit, emit)),
DriverKind::Redis => Err(ConnectionError::UnsupportedDriver { driver }),
DriverKind::Redis | DriverKind::Mongo => {
Err(ConnectionError::UnsupportedDriver { driver })
}
}
}

Expand Down Expand Up @@ -356,6 +408,9 @@ impl ConnectionRegistry {
DriverKind::Redis => Err(ConnectionError::UnsupportedDriver {
driver: DriverKind::Redis,
}),
DriverKind::Mongo => Err(ConnectionError::UnsupportedDriver {
driver: DriverKind::Mongo,
}),
}
}

Expand Down Expand Up @@ -403,6 +458,9 @@ impl ConnectionRegistry {
DriverKind::Redis => Err(ConnectionError::UnsupportedDriver {
driver: DriverKind::Redis,
}),
DriverKind::Mongo => Err(ConnectionError::UnsupportedDriver {
driver: DriverKind::Mongo,
}),
}
}
}
Expand Down
Loading
Loading