feat(mongo): add database and collection deletion

This commit is contained in:
林剑凛 2026-06-20 22:22:04 +08:00 committed by GitHub
parent d1fb9b8445
commit 41edb3da91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 282 additions and 1 deletions

View File

@ -747,6 +747,14 @@ function requestDeleteSelectedNode(): boolean {
dropDatabase();
return true;
}
if (canDropMongoDatabase.value) {
dropDatabase();
return true;
}
if (canDropMongoCollection.value) {
dropMongoCollection();
return true;
}
if (canDropSchema.value) {
dropSchema();
return true;
@ -1276,6 +1284,8 @@ const createDatabaseCharset = ref("utf8mb4");
const createDatabaseCollation = ref("utf8mb4_unicode_ci");
const showDropDatabaseConfirm = ref(false);
const dropDatabaseLoading = ref(false);
const showDropMongoCollectionConfirm = ref(false);
const dropMongoCollectionLoading = ref(false);
const showFlushRedisDbConfirm = ref(false);
const showCreateSchemaDialog = ref(false);
const createSchemaName = ref("");
@ -1745,7 +1755,7 @@ const canCreateTable = computed(() => {
const canCreateDatabase = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
return props.node.type === "connection" && (supportsDatabaseCreation(config?.db_type) || config?.db_type === "duckdb");
return props.node.type === "connection" && (supportsDatabaseCreation(config?.db_type) || config?.db_type === "duckdb" || (config?.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy"));
});
const isDuckDbConnection = computed(() => {
@ -1763,6 +1773,16 @@ const canDropDatabase = computed(() => {
return props.node.type === "database" && !isSqlServerLinkedNode(props.node) && supportsDatabaseCreation(config?.db_type);
});
const canDropMongoDatabase = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
return props.node.type === "mongo-db" && !!props.node.database && config?.driver_profile !== "mongodb-legacy";
});
const canDropMongoCollection = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
return props.node.type === "mongo-collection" && !!props.node.database && config?.driver_profile !== "mongodb-legacy";
});
const canCreateSchema = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
return props.node.type === "database" && usesTreeSchemaMode(effectiveDatabaseTypeForConnection(config)) && !connectionUsesDatabaseObjectTreeMode(config);
@ -1857,6 +1877,10 @@ async function confirmTruncateTable() {
}
async function refreshDropDatabasePreviewSql() {
if (props.node.type === "mongo-db") {
dropDatabasePreviewSql.value = `db.getSiblingDB(${JSON.stringify(props.node.label)}).dropDatabase();`;
return;
}
dropDatabasePreviewSql.value = "";
dropDatabasePreviewSql.value = await buildDropDatabaseSql({
databaseType: currentDatabaseType(),
@ -1939,6 +1963,12 @@ async function confirmCreateDatabase() {
try {
await connectionStore.ensureConnected(node.connectionId);
const config = connectionStore.getConfig(node.connectionId);
if (config?.db_type === "mongodb") {
await api.mongoCreateDatabase(node.connectionId, name);
toast(t("contextMenu.createDatabaseSuccess", { name }), 3000);
await connectionStore.loadMongoDatabases(node.connectionId);
return;
}
const sql = await buildCreateDatabaseSql({
databaseType: config?.db_type,
driverProfile: config?.driver_profile,
@ -1960,6 +1990,11 @@ function dropDatabase() {
showDropDatabaseConfirm.value = true;
}
function dropMongoCollection() {
dropMongoCollectionLoading.value = false;
showDropMongoCollectionConfirm.value = true;
}
function flushRedisDb() {
showFlushRedisDbConfirm.value = true;
}
@ -1988,6 +2023,13 @@ async function confirmDropDatabase() {
dropDatabaseLoading.value = true;
try {
await connectionStore.ensureConnected(node.connectionId);
if (node.type === "mongo-db" && node.database) {
await api.mongoDropDatabase(node.connectionId, node.database);
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
await connectionStore.loadMongoDatabases(node.connectionId);
showDropDatabaseConfirm.value = false;
return;
}
const sql =
dropDatabasePreviewSql.value ||
(await buildDropDatabaseSql({
@ -2005,6 +2047,23 @@ async function confirmDropDatabase() {
}
}
async function confirmDropMongoCollection() {
const node = props.node;
if (node.type !== "mongo-collection" || !node.connectionId || !node.database || dropMongoCollectionLoading.value) return;
dropMongoCollectionLoading.value = true;
try {
await connectionStore.ensureConnected(node.connectionId);
await api.mongoDropCollection(node.connectionId, node.database, node.label);
toast(t("contextMenu.dropCollectionSuccess", { name: node.label }), 3000);
await connectionStore.loadMongoCollections(node.connectionId, node.database);
showDropMongoCollectionConfirm.value = false;
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
} finally {
dropMongoCollectionLoading.value = false;
}
}
function openCreateSchemaDialog() {
createSchemaName.value = "";
showCreateSchemaDialog.value = true;
@ -3263,6 +3322,22 @@ function treeItemMenuItems(): ContextMenuItem[] {
items.push({ label: "", separator: true });
items.push({ label: t("redis.flushDb"), action: flushRedisDb, icon: Eraser, variant: "destructive" as const });
}
if (canDropMongoDatabase.value) {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.dropDatabase"), action: dropDatabase, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
}
return items;
}
if (node.type === "mongo-collection") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: toggle, icon: TableProperties });
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (canDropMongoCollection.value) {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.dropCollection"), action: dropMongoCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
}
return items;
}
@ -3779,6 +3854,16 @@ function treeItemMenuItems(): ContextMenuItem[] {
@confirm="confirmDropDatabase"
/>
<DangerConfirmDialog
v-model:open="showDropMongoCollectionConfirm"
:title="t('contextMenu.confirmDropCollectionTitle')"
:message="t('contextMenu.confirmDropCollectionMessage', { name: node.label })"
:confirm-label="t('contextMenu.dropCollection')"
:loading="dropMongoCollectionLoading"
:close-on-confirm="false"
@confirm="confirmDropMongoCollection"
/>
<DangerConfirmDialog v-model:open="showFlushRedisDbConfirm" :title="t('redis.flushDb')" :message="t('redis.flushDbMessage')" :details="t('redis.flushDbDetails', { db: node.database })" :confirm-label="t('redis.flushDbConfirm')" @confirm="confirmFlushRedisDb" />
<Dialog v-model:open="showCreateSchemaDialog">

View File

@ -1245,6 +1245,10 @@ export default {
createDuckDbFileSuccess: 'DuckDB database file "{name}" created and attached',
createDuckDbFileDesktopOnly: "Creating DuckDB database files is only available in the desktop app",
dropDatabaseSuccess: 'Database "{name}" dropped',
dropCollection: "Drop Collection",
confirmDropCollectionTitle: "Drop Collection",
confirmDropCollectionMessage: 'Are you sure you want to drop collection "{name}"? This will permanently delete the collection and all its documents.',
dropCollectionSuccess: 'Collection "{name}" dropped',
createDatabaseNamePlaceholder: "Database name",
createDatabaseCharset: "Character set",
createDatabaseCharsetPlaceholder: "e.g. utf8mb4",

View File

@ -1070,6 +1070,10 @@ export default {
createDuckDbFileSuccess: 'Archivo de base de datos DuckDB "{name}" creado y adjuntado',
createDuckDbFileDesktopOnly: "Crear archivos de base de datos DuckDB solo está disponible en la app de escritorio",
dropDatabaseSuccess: 'Base de datos "{name}" eliminada',
dropCollection: "Eliminar colección",
confirmDropCollectionTitle: "Eliminar colección",
confirmDropCollectionMessage: '¿Estás seguro de que deseas eliminar la colección "{name}"? Esto borrará permanentemente la colección y todos sus documentos.',
dropCollectionSuccess: 'Colección "{name}" eliminada',
createDatabaseNamePlaceholder: "Nombre de la base de datos",
createSchema: "Crear esquema",
dropSchema: "Eliminar esquema",

View File

@ -1182,6 +1182,10 @@ export default {
createDuckDbFileSuccess: 'File di database DuckDB "{name}" creato e collegato',
createDuckDbFileDesktopOnly: "La creazione di file di database DuckDB è disponibile solo nell'applicazione desktop",
dropDatabaseSuccess: 'Database "{name}" eliminato',
dropCollection: "Elimina Collection",
confirmDropCollectionTitle: "Elimina Collection",
confirmDropCollectionMessage: 'Sei sicuro di voler eliminare la collection "{name}"? Questa operazione eliminerà permanentemente la collection e tutti i suoi documenti.',
dropCollectionSuccess: 'Collection "{name}" eliminata',
createDatabaseNamePlaceholder: "Nome database",
createDatabaseCharset: "Set di caratteri",
createDatabaseCharsetPlaceholder: "es. utf8mb4",

View File

@ -1234,6 +1234,10 @@ export default {
createDuckDbFileSuccess: "DuckDBデータベースファイル「{name}」を作成し、アタッチしました",
createDuckDbFileDesktopOnly: "DuckDBデータベースファイルの作成はデスクトップアプリでのみ利用可能です",
dropDatabaseSuccess: "データベース「{name}」を削除しました",
dropCollection: "コレクションを削除",
confirmDropCollectionTitle: "コレクションを削除",
confirmDropCollectionMessage: "本当にコレクション「{name}」を削除しますか?コレクションとすべてのドキュメントが永久に削除されます。",
dropCollectionSuccess: "コレクション「{name}」を削除しました",
createDatabaseNamePlaceholder: "データベース名",
createDatabaseCharset: "文字セット",
createDatabaseCharsetPlaceholder: "例: utf8mb4",

View File

@ -1182,6 +1182,10 @@ export default {
createDuckDbFileSuccess: 'Arquivo de banco de dados DuckDB "{name}" criado e anexado',
createDuckDbFileDesktopOnly: "A criação de arquivos de banco de dados DuckDB só está disponível no aplicativo desktop",
dropDatabaseSuccess: 'Banco de dados "{name}" removido',
dropCollection: "Remover coleção",
confirmDropCollectionTitle: "Remover coleção",
confirmDropCollectionMessage: 'Tem certeza de que deseja remover a coleção "{name}"? Isso excluirá permanentemente a coleção e todos os seus documentos.',
dropCollectionSuccess: 'Coleção "{name}" removida',
createDatabaseNamePlaceholder: "Nome do banco de dados",
createDatabaseCharset: "Conjunto de caracteres",
createDatabaseCharsetPlaceholder: "por exemplo, utf8mb4",

View File

@ -1244,6 +1244,10 @@ export default {
createDuckDbFileSuccess: "DuckDB 数据库文件「{name}」已创建并附加",
createDuckDbFileDesktopOnly: "新建 DuckDB 数据库文件仅支持桌面端",
dropDatabaseSuccess: "数据库「{name}」已删除",
dropCollection: "删除集合",
confirmDropCollectionTitle: "删除集合",
confirmDropCollectionMessage: "确定要删除集合「{name}」吗?这将永久删除该集合及其所有文档。",
dropCollectionSuccess: "集合「{name}」已删除",
createDatabaseNamePlaceholder: "数据库名称",
createDatabaseCharset: "字符集",
createDatabaseCharsetPlaceholder: "例如 utf8mb4",

View File

@ -1162,6 +1162,10 @@ export default {
createDuckDbFileSuccess: "DuckDB 資料庫檔案「{name}」已建立並附加",
createDuckDbFileDesktopOnly: "建立 DuckDB 資料庫檔案僅支援桌面端",
dropDatabaseSuccess: "資料庫「{name}」已刪除",
dropCollection: "刪除集合",
confirmDropCollectionTitle: "刪除集合",
confirmDropCollectionMessage: "確定要刪除集合「{name}」嗎?這將永久刪除該集合及其所有文件。",
dropCollectionSuccess: "集合「{name}」已刪除",
createDatabaseNamePlaceholder: "資料庫名稱",
createDatabaseCharset: "字元集",
createDatabaseCharsetPlaceholder: "例如 utf8mb4",

View File

@ -330,6 +330,9 @@ export const mqRawRequest = forward("mqRawRequest");
// MongoDB
export const mongoListDatabases = forward("mongoListDatabases");
export const mongoListCollections = forward("mongoListCollections");
export const mongoCreateDatabase = forward("mongoCreateDatabase");
export const mongoDropDatabase = forward("mongoDropDatabase");
export const mongoDropCollection = forward("mongoDropCollection");
export const documentFindDocuments = forward("documentFindDocuments");
export const mongoFindDocuments = forward("mongoFindDocuments");
export const mongoAggregateDocuments = forward("mongoAggregateDocuments");

View File

@ -1486,6 +1486,18 @@ export async function mongoListCollections(connectionId: string, database: strin
return post("/api/mongo/list-collections", { connectionId, database });
}
export async function mongoCreateDatabase(connectionId: string, database: string): Promise<void> {
await post("/api/mongo/create-database", { connectionId, database });
}
export async function mongoDropDatabase(connectionId: string, database: string): Promise<void> {
await post("/api/mongo/drop-database", { connectionId, database });
}
export async function mongoDropCollection(connectionId: string, database: string, collection: string): Promise<void> {
await post("/api/mongo/drop-collection", { connectionId, database, collection });
}
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
return mongoListCollections(connectionId, "default");
}

View File

@ -1323,6 +1323,18 @@ export async function mongoListCollections(connectionId: string, database: strin
return invoke("mongo_list_collections", { connectionId, database });
}
export async function mongoCreateDatabase(connectionId: string, database: string): Promise<void> {
return invoke("mongo_create_database", { connectionId, database });
}
export async function mongoDropDatabase(connectionId: string, database: string): Promise<void> {
return invoke("mongo_drop_database", { connectionId, database });
}
export async function mongoDropCollection(connectionId: string, database: string, collection: string): Promise<void> {
return invoke("mongo_drop_collection", { connectionId, database, collection });
}
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
return mongoListCollections(connectionId, "default");
}

View File

@ -82,6 +82,34 @@ pub async fn list_collections(client: &Client, database: &str) -> Result<Vec<Str
client.database(database).list_collection_names().await.map_err(|e| e.to_string())
}
pub async fn create_database(client: &Client, database: &str) -> Result<(), String> {
let database = database.trim();
if database.is_empty() {
return Err("Database name is required".to_string());
}
client.database(database).create_collection("dbx_init").await.map_err(|e| e.to_string())
}
pub async fn drop_database(client: &Client, database: &str) -> Result<(), String> {
let database = database.trim();
if database.is_empty() {
return Err("Database name is required".to_string());
}
client.database(database).drop().await.map_err(|e| e.to_string())
}
pub async fn drop_collection(client: &Client, database: &str, collection: &str) -> Result<(), String> {
let database = database.trim();
let collection = collection.trim();
if database.is_empty() {
return Err("Database name is required".to_string());
}
if collection.is_empty() {
return Err("Collection name is required".to_string());
}
client.database(database).collection::<Document>(collection).drop().await.map_err(|e| e.to_string())
}
pub async fn list_indexes(client: &Client, database: &str, collection: &str) -> Result<Vec<IndexInfo>, String> {
let col = client.database(database).collection::<Document>(collection);
let mut cursor = col.list_indexes().await.map_err(|e| e.to_string())?;

View File

@ -79,6 +79,41 @@ pub async fn mongo_list_collections_core(
}
}
pub async fn mongo_create_database_core(state: &AppState, connection_id: &str, database: &str) -> Result<(), String> {
ensure_document_pool(state, connection_id).await?;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::create_database(client, database).await,
PoolKind::Agent(_) => Err("MongoDB legacy agent does not support create database".to_string()),
_ => Err("Not a MongoDB connection".to_string()),
}
}
pub async fn mongo_drop_database_core(state: &AppState, connection_id: &str, database: &str) -> Result<(), String> {
ensure_document_pool(state, connection_id).await?;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::drop_database(client, database).await,
PoolKind::Agent(_) => Err("MongoDB legacy agent does not support drop database".to_string()),
_ => Err("Not a MongoDB connection".to_string()),
}
}
pub async fn mongo_drop_collection_core(
state: &AppState,
connection_id: &str,
database: &str,
collection: &str,
) -> Result<(), String> {
ensure_document_pool(state, connection_id).await?;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::drop_collection(client, database, collection).await,
PoolKind::Agent(_) => Err("MongoDB legacy agent does not support drop collection".to_string()),
_ => Err("Not a MongoDB connection".to_string()),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn document_find_documents_core(
state: &AppState,

View File

@ -335,6 +335,9 @@ async fn main() {
// MongoDB
.route("/mongo/list-databases", post(routes::mongo::list_databases))
.route("/mongo/list-collections", post(routes::mongo::list_collections))
.route("/mongo/create-database", post(routes::mongo::create_database))
.route("/mongo/drop-database", post(routes::mongo::drop_database))
.route("/mongo/drop-collection", post(routes::mongo::drop_collection))
.route("/document-store/find-documents", post(routes::mongo::document_find_documents))
.route("/mongo/find-documents", post(routes::mongo::find_documents))
.route("/mongo/aggregate-documents", post(routes::mongo::aggregate_documents))

View File

@ -56,6 +56,14 @@ pub struct MongoCollectionRequest {
pub database: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MongoCollectionNameRequest {
pub connection_id: String,
pub database: String,
pub collection: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MongoFindRequest {
@ -157,6 +165,39 @@ pub async fn list_collections(
Ok(Json(result))
}
pub async fn create_database(
State(state): State<Arc<WebState>>,
Json(req): Json<MongoCollectionRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
ensure_writable(&state.app, &req.connection_id, "Create database").await?;
dbx_core::mongo_ops::mongo_create_database_core(&state.app, &req.connection_id, &req.database)
.await
.map_err(AppError)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn drop_database(
State(state): State<Arc<WebState>>,
Json(req): Json<MongoCollectionRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
ensure_writable(&state.app, &req.connection_id, "Drop database").await?;
dbx_core::mongo_ops::mongo_drop_database_core(&state.app, &req.connection_id, &req.database)
.await
.map_err(AppError)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn drop_collection(
State(state): State<Arc<WebState>>,
Json(req): Json<MongoCollectionNameRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
ensure_writable(&state.app, &req.connection_id, "Drop collection").await?;
dbx_core::mongo_ops::mongo_drop_collection_core(&state.app, &req.connection_id, &req.database, &req.collection)
.await
.map_err(AppError)?;
Ok(Json(serde_json::json!({ "ok": true })))
}
pub async fn find_documents(
State(state): State<Arc<WebState>>,
Json(req): Json<MongoFindRequest>,

View File

@ -40,6 +40,37 @@ pub async fn mongo_list_collections(
dbx_core::mongo_ops::mongo_list_collections_core(&state, &connection_id, &database).await
}
#[tauri::command]
pub async fn mongo_create_database(
state: State<'_, Arc<AppState>>,
connection_id: String,
database: String,
) -> Result<(), String> {
ensure_connection_writable(&state, &connection_id, "Create database").await?;
dbx_core::mongo_ops::mongo_create_database_core(&state, &connection_id, &database).await
}
#[tauri::command]
pub async fn mongo_drop_database(
state: State<'_, Arc<AppState>>,
connection_id: String,
database: String,
) -> Result<(), String> {
ensure_connection_writable(&state, &connection_id, "Drop database").await?;
dbx_core::mongo_ops::mongo_drop_database_core(&state, &connection_id, &database).await
}
#[tauri::command]
pub async fn mongo_drop_collection(
state: State<'_, Arc<AppState>>,
connection_id: String,
database: String,
collection: String,
) -> Result<(), String> {
ensure_connection_writable(&state, &connection_id, "Drop collection").await?;
dbx_core::mongo_ops::mongo_drop_collection_core(&state, &connection_id, &database, &collection).await
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub async fn mongo_find_documents(

View File

@ -535,6 +535,9 @@ pub fn run() {
commands::sqlite_backup::backup_sqlite_database,
commands::mongo_cmd::mongo_list_databases,
commands::mongo_cmd::mongo_list_collections,
commands::mongo_cmd::mongo_create_database,
commands::mongo_cmd::mongo_drop_database,
commands::mongo_cmd::mongo_drop_collection,
commands::mongo_cmd::document_find_documents,
commands::mongo_cmd::mongo_find_documents,
commands::mongo_cmd::mongo_aggregate_documents,