fix(mongodb): support show databases shell commands

Closes #6226
This commit is contained in:
t8y2
2026-08-14 07:49:07 +00:00
parent 462f8e1fb2
commit 65792128e4
7 changed files with 515 additions and 1 deletions
@@ -62,6 +62,10 @@ export interface MongoVersionCommand {
kind: "version";
}
export interface MongoShowDatabasesCommand {
kind: "showDatabases";
}
export interface MongoCreateUserCommand {
userJson: string;
writeConcernJson?: string;
@@ -91,6 +95,7 @@ export type MongoCommand =
| ({ kind: "find" } & MongoFindCommand)
| ({ kind: "findOne" } & MongoFindOneCommand)
| MongoVersionCommand
| MongoShowDatabasesCommand
| ({ kind: "countDocuments" } & MongoCountDocumentsCommand)
| ({ kind: "aggregate" } & MongoAggregateCommand)
| ({ kind: "distinct" } & MongoDistinctCommand)
@@ -619,6 +624,7 @@ export function parseMongoCommand(input: string): ParsedMongoCommand | null {
// Keep the more specific readers ahead of generic write parsing so the
// returned kind matches the result renderer we want to use downstream.
const parsers: Array<(source: string) => MongoCommand | null> = [
parseMongoShowDatabasesCommand,
(source) => {
const version = parseMongoVersionCommand(source);
return version ?? null;
@@ -691,6 +697,11 @@ export function parseMongoCommand(input: string): ParsedMongoCommand | null {
return null;
}
export function parseMongoShowDatabasesCommand(input: string): MongoShowDatabasesCommand | null {
const source = input.trim().replace(/;$/, "").trim();
return /^show\s+(?:dbs|databases)$/i.test(source) ? { kind: "showDatabases" } : null;
}
export function splitMongoCommands(input: string): ParsedMongoCommand[] {
return splitMongoCommandRanges(input).map(({ from: _from, to: _to, ...command }) => command);
}
@@ -786,6 +797,27 @@ export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeM
};
}
export function mongoDatabasesToQueryResult(documents: unknown[], executionTimeMs: number, maxRows: number): QueryResult {
const response = documents[0];
const databases = isRecord(response) ? response.databases : undefined;
if (!Array.isArray(databases)) throw new Error("MongoDB listDatabases response is missing the databases array.");
if (!databases.every(isRecord)) {
throw new Error("MongoDB listDatabases response contains an invalid database entry.");
}
const rowLimit = Number.isFinite(maxRows) ? Math.max(1, Math.trunc(maxRows)) : databases.length;
const rows = databases.slice(0, rowLimit).map((database) => [toCellValue(database.name), toCellValue(database.sizeOnDisk), toCellValue(database.empty)]);
const truncated = rows.length < databases.length;
return {
columns: ["name", "sizeOnDisk", "empty"],
rows,
affected_rows: databases.length,
execution_time_ms: Math.max(0, Math.round(executionTimeMs)),
truncated,
has_more: truncated,
};
}
export function mongoDistinctToQueryResult(field: string, values: unknown[], executionTimeMs: number): QueryResult {
return {
columns: [field],
@@ -1110,7 +1142,7 @@ function mongoTopLevelCommandLineStarts(segment: string): number[] {
function isMongoCommandLineStart(segment: string, index: number): boolean {
const rest = segment.slice(index);
return /^use\b/i.test(rest) || /^db(?:\s*\.|\b)/i.test(rest);
return /^use\b/i.test(rest) || /^show\s+(?:dbs|databases)\b/i.test(rest) || /^db(?:\s*\.|\b)/i.test(rest);
}
function pushMongoSegment(segments: MongoTextRange[], source: string, from: number, to: number) {
+15
View File
@@ -15,6 +15,7 @@ import {
evaluateMongoWriteSafety,
mongoCollectionStatsToQueryResult,
mongoCountToQueryResult,
mongoDatabasesToQueryResult,
mongoDistinctToQueryResult,
mongoCreateIndexToQueryResult,
mongoDocumentsToQueryResult,
@@ -4092,6 +4093,7 @@ export const useQueryStore = defineStore("query", () => {
queryExecutionLog("info", "mongo:start", { traceId, commandCount: mongoCommands.length, sqlLength: sql.length });
const allResults: QueryResult[] = [];
const mongoResultMaxRows = effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows);
// Track the effective db as we walk the batch so later commands observe
// earlier `use ...` statements in the same editor selection.
let currentDatabase = executionTarget?.database ?? tab.database;
@@ -4175,6 +4177,19 @@ export const useQueryStore = defineStore("query", () => {
});
break;
}
case "showDatabases": {
queryExecutionLog("info", "mongo-show-databases:start", { traceId });
const result = await api.mongoRunCommand(executionConnectionId, "admin", '{"listDatabases":1}', executionId);
const queryResult = mongoDatabasesToQueryResult(result.documents, performance.now() - commandStartedAt, agentProtocolQueryResultMaxRows(mongoResultMaxRows));
allResults.push(markQueryResultRowsRaw(annotateMongoResult(queryResult)));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-show-databases:done", {
traceId,
databaseCount: queryResult.affected_rows,
elapsed: elapsed(),
});
break;
}
case "countDocuments": {
queryExecutionLog("info", "mongo-count:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const total = await api.mongoCountDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.mode, executionId);
+139
View File
@@ -8,6 +8,9 @@ use crate::document_ops::CollectionInfo;
use crate::mongo_shell::MongoCommand;
use crate::types::{IndexInfo, QueryResult};
pub const MONGO_SHOW_DATABASES_DATABASE: &str = "admin";
pub const MONGO_SHOW_DATABASES_COMMAND_JSON: &str = r#"{"listDatabases":1}"#;
async fn ensure_document_pool(state: &AppState, connection_id: &str) -> Result<(), String> {
state.get_or_create_pool(connection_id, None).await.map(|_| ())
}
@@ -177,6 +180,10 @@ pub async fn mongo_run_command_core(
}
}
pub async fn mongo_show_databases_core(state: &AppState, connection_id: &str) -> Result<MongoDocumentResult, String> {
mongo_run_command_core(state, connection_id, MONGO_SHOW_DATABASES_DATABASE, MONGO_SHOW_DATABASES_COMMAND_JSON).await
}
pub async fn mongo_collection_stats_core(
state: &AppState,
connection_id: &str,
@@ -918,6 +925,10 @@ pub async fn execute_mongo_command_core(
.await
.map(|version| scalar_query_result("version", Value::String(version))),
MongoCommand::Use { database } => Ok(scalar_query_result("database", Value::String(database.clone()))),
MongoCommand::ShowDatabases => {
let result = mongo_show_databases_core(state, connection_id).await?;
mongo_show_databases_query_result(result.documents, max_rows)
}
MongoCommand::RunCommand { command_json } => {
let result = mongo_run_command_core(state, connection_id, database, command_json).await?;
Ok(mongo_documents_query_result(result.documents))
@@ -1233,6 +1244,41 @@ fn mongo_documents_query_result(documents: Vec<serde_json::Value>) -> QueryResul
query_result(columns, rows, 0)
}
pub fn mongo_show_databases_query_result(
documents: Vec<serde_json::Value>,
max_rows: usize,
) -> Result<QueryResult, String> {
use serde_json::Value;
let databases = documents
.first()
.and_then(Value::as_object)
.and_then(|response| response.get("databases"))
.and_then(Value::as_array)
.ok_or("MongoDB listDatabases response is missing the databases array")?;
if databases.iter().any(|database| !database.is_object()) {
return Err("MongoDB listDatabases response contains an invalid database entry".to_string());
}
let total = databases.len();
let rows = databases
.iter()
.take(max_rows.max(1))
.map(|database| {
let database = database.as_object().expect("database entries were validated above");
["name", "sizeOnDisk", "empty"].map(|field| database.get(field).cloned().unwrap_or(Value::Null)).to_vec()
})
.collect::<Vec<_>>();
let mut query_result = query_result(
vec!["name".to_string(), "sizeOnDisk".to_string(), "empty".to_string()],
rows,
u64::try_from(total).unwrap_or(u64::MAX),
);
query_result.truncated = query_result.rows.len() < total;
query_result.has_more = query_result.truncated;
Ok(query_result)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1261,6 +1307,63 @@ mod tests {
assert_eq!(limited.extended_documents.unwrap(), vec![serde_json::json!(1), serde_json::json!(2)]);
}
#[test]
fn show_databases_result_preserves_metadata_and_row_limit() {
let result = MongoDocumentResult {
documents: vec![serde_json::json!({
"databases": [
{"name": "admin", "sizeOnDisk": 40960, "empty": false},
{"name": "app", "sizeOnDisk": 8192, "empty": true},
{"name": "logs", "sizeOnDisk": 1024, "empty": false}
],
"totalSize": 50176,
"ok": 1
})],
raw_documents: None,
extended_documents: None,
total: 1,
total_is_exact: true,
};
let query_result = mongo_show_databases_query_result(result.documents, 2).unwrap();
assert_eq!(query_result.columns, ["name", "sizeOnDisk", "empty"]);
assert_eq!(
query_result.rows,
[
[serde_json::json!("admin"), serde_json::json!(40960), serde_json::json!(false)],
[serde_json::json!("app"), serde_json::json!(8192), serde_json::json!(true)],
]
);
assert_eq!(query_result.affected_rows, 3);
assert!(query_result.truncated);
assert!(query_result.has_more);
}
#[test]
fn show_databases_result_handles_empty_and_rejects_malformed_responses() {
let empty = MongoDocumentResult {
documents: vec![serde_json::json!({"databases": [], "ok": 1})],
raw_documents: None,
extended_documents: None,
total: 1,
total_is_exact: true,
};
let query_result = mongo_show_databases_query_result(empty.documents, 100).unwrap();
assert_eq!(query_result.columns, ["name", "sizeOnDisk", "empty"]);
assert!(query_result.rows.is_empty());
assert!(!query_result.truncated);
let malformed = MongoDocumentResult {
documents: vec![serde_json::json!({"ok": 1})],
raw_documents: None,
extended_documents: None,
total: 1,
total_is_exact: true,
};
assert!(mongo_show_databases_query_result(malformed.documents, 100).unwrap_err().contains("databases array"));
}
#[cfg(unix)]
use crate::db::agent_driver::{AgentDriverClient, AgentLaunchSpec};
#[cfg(unix)]
@@ -1365,6 +1468,7 @@ EXPECTED_RESULT = json.loads({expected_result})
CAPABILITIES = {capabilities}
SERVER_VERSION = {server_version}
EXPECTED_ERROR = {expected_error}
expected_calls = 0
print(json.dumps({{"ready": True}}), flush=True)
for line in sys.stdin:
@@ -1382,6 +1486,10 @@ for line in sys.stdin:
if request.get("method") != EXPECTED_METHOD or request.get("params") != EXPECTED_PARAMS:
print(json.dumps({{"jsonrpc": "2.0", "id": request["id"], "error": {{"code": -1, "message": "unexpected MongoDB RPC"}}}}), flush=True)
continue
expected_calls += 1
if expected_calls > 1:
print(json.dumps({{"jsonrpc": "2.0", "id": request["id"], "error": {{"code": -1, "message": "duplicate MongoDB RPC"}}}}), flush=True)
continue
if EXPECTED_ERROR is not None:
print(json.dumps({{"jsonrpc": "2.0", "id": request["id"], "error": {{"code": -1, "message": EXPECTED_ERROR}}}}), flush=True)
else:
@@ -1477,6 +1585,37 @@ for line in sys.stdin:
);
}
#[cfg(unix)]
#[tokio::test]
async fn mongo_show_databases_uses_one_admin_agent_command() {
let expected_result = serde_json::json!({
"documents": [{
"databases": [{"name": "admin", "sizeOnDisk": 40960, "empty": false}],
"ok": 1
}],
"total": 1,
});
let (state, _directory) = legacy_mongo_state_with_capabilities(
"run_command",
serde_json::json!({
"database": "admin",
"command_json": "{\"listDatabases\":1}",
}),
expected_result,
&[AgentCapability::MongoRunCommand.as_str()],
)
.await;
let query_result =
execute_mongo_command_core(&state, "legacy", "ignored-current-database", &MongoCommand::ShowDatabases, 100)
.await
.unwrap();
assert_eq!(query_result.columns, ["name", "sizeOnDisk", "empty"]);
assert_eq!(query_result.rows.len(), 1);
assert_eq!(query_result.rows[0][0], "admin");
}
#[cfg(unix)]
#[tokio::test]
async fn mongo_run_command_requires_an_explicit_legacy_agent_capability() {
+109
View File
@@ -8,6 +8,8 @@ pub enum MongoCommand {
Version,
#[serde(rename = "use")]
Use { database: String },
#[serde(rename = "showDatabases")]
ShowDatabases,
#[serde(rename = "runCommand")]
RunCommand {
#[serde(rename = "commandJson")]
@@ -355,6 +357,10 @@ fn mongo_field_predicate_is_exists_true(value: &serde_json::Value) -> bool {
}
pub fn parse(input: &str) -> Result<MongoCommand, String> {
let show_source = trim_mongo_outer_comments(input).trim_end_matches(';').trim();
if parse_show_databases(show_source) {
return Ok(MongoCommand::ShowDatabases);
}
let source = input.trim().trim_end_matches(';').trim();
if source.eq_ignore_ascii_case("db.version()") {
return Ok(MongoCommand::Version);
@@ -645,6 +651,86 @@ pub fn parse(input: &str) -> Result<MongoCommand, String> {
Err("Unsupported MongoDB shell command.".to_string())
}
fn parse_show_databases(source: &str) -> bool {
let mut words = source.split_whitespace();
words.next().is_some_and(|word| word.eq_ignore_ascii_case("show"))
&& words.next().is_some_and(|word| word.eq_ignore_ascii_case("dbs") || word.eq_ignore_ascii_case("databases"))
&& words.next().is_none()
}
fn trim_mongo_outer_comments(mut source: &str) -> &str {
loop {
source = source.trim_start();
if source.starts_with("//") || source.starts_with("--") {
source = source
.char_indices()
.find_map(|(index, character)| (character == '\n' || character == '\r').then_some(&source[index + 1..]))
.unwrap_or("");
continue;
}
if source.starts_with("/*") {
let Some(end) = source.find("*/") else {
return source;
};
source = &source[end + 2..];
continue;
}
break;
}
let source = source.trim_end();
let mut body_end = 0;
let mut quote = None;
let mut escaped = false;
let mut line_comment = false;
let mut block_comment = false;
let mut characters = source.char_indices().peekable();
while let Some((index, character)) = characters.next() {
let next = characters.peek().map(|(_, character)| *character);
if line_comment {
if character == '\n' || character == '\r' {
line_comment = false;
}
continue;
}
if block_comment {
if character == '*' && next == Some('/') {
block_comment = false;
characters.next();
}
continue;
}
if let Some(delimiter) = quote {
body_end = index + character.len_utf8();
if escaped {
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == delimiter {
quote = None;
}
continue;
}
if (character == '/' && next == Some('/')) || (character == '-' && next == Some('-')) {
line_comment = true;
characters.next();
continue;
}
if character == '/' && next == Some('*') {
block_comment = true;
characters.next();
continue;
}
if matches!(character, '"' | '\'' | '`') {
quote = Some(character);
}
if !character.is_whitespace() {
body_end = index + character.len_utf8();
}
}
source[..body_end].trim_end()
}
fn parse_collection_prefix(source: &str) -> Result<(String, usize), String> {
if !source.get(..3).is_some_and(|prefix| prefix.eq_ignore_ascii_case("db.")) {
return Err("MongoDB command must start with db.<collection>.".to_string());
@@ -1064,6 +1150,27 @@ mod tests {
assert_eq!(validate_safety(&command, true, true, false), Ok(()));
}
#[test]
fn parses_show_databases_aliases_as_read_only() {
for source in [
"show dbs",
"SHOW DATABASES;",
"ShOw DbS ;",
"/* databases */ show dbs -- list",
"// databases\nshow databases; /* list */",
] {
let command = parse(source).unwrap();
assert_eq!(command, MongoCommand::ShowDatabases, "{source}");
assert!(!command.is_mutating(), "{source}");
assert!(!command.is_dangerous(), "{source}");
assert_eq!(validate_safety(&command, false, false, true), Ok(()), "{source}");
}
for source in ["show dbs extra", "show database", "show collections", "show"] {
assert!(parse(source).is_err(), "{source}");
}
}
#[test]
fn rejects_unsupported_run_command_shapes() {
for source in [
@@ -1230,6 +1337,8 @@ mod tests {
let run_command = serde_json::to_value(parse("db.runCommand({ping: 1})").unwrap()).unwrap();
assert_eq!(run_command["kind"], "runCommand");
assert_eq!(run_command["commandJson"], r#"{"ping":1}"#);
let show_databases = serde_json::to_value(parse("show dbs").unwrap()).unwrap();
assert_eq!(show_databases["kind"], "showDatabases");
}
#[test]
+97
View File
@@ -1006,6 +1006,23 @@ impl DbxBackend for WebBackend {
Ok(scalar_query_result("version", Value::String(version)))
}
MongoCommand::Use { database } => Ok(scalar_query_result("database", Value::String(database.clone()))),
MongoCommand::ShowDatabases => {
let result = self
.request(
reqwest::Method::POST,
"/api/mongo/run-command",
Some(json!({
"connectionId": connection_id,
"database": dbx_core::mongo_ops::MONGO_SHOW_DATABASES_DATABASE,
"commandJson": dbx_core::mongo_ops::MONGO_SHOW_DATABASES_COMMAND_JSON,
})),
)
.await?
.json::<WebMongoDocuments>()
.await
.map_err(|error| format!("Invalid MongoDB listDatabases response: {error}"))?;
dbx_core::mongo_ops::mongo_show_databases_query_result(result.documents, 100)
}
MongoCommand::RunCommand { .. } => {
Err("MongoDB runCommand is not available through the DBX MCP backend".to_string())
}
@@ -1863,6 +1880,86 @@ mod tests {
assert_eq!(result.affected_rows, 1);
}
#[tokio::test]
async fn web_mongo_show_databases_uses_one_admin_read_command() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let (request_sender, request_receiver) = mpsc::channel();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut request = Vec::new();
let mut buffer = [0_u8; 1024];
let header_end = loop {
let count = stream.read(&mut buffer).unwrap();
request.extend_from_slice(&buffer[..count]);
if let Some(position) = request.windows(4).position(|window| window == b"\r\n\r\n") {
break position + 4;
}
};
let headers = String::from_utf8_lossy(&request[..header_end]);
let content_length = headers
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("content-length").then_some(value.trim())
})
.unwrap()
.parse::<usize>()
.unwrap();
while request.len() < header_end + content_length {
let count = stream.read(&mut buffer).unwrap();
request.extend_from_slice(&buffer[..count]);
}
let request = String::from_utf8(request).unwrap();
request_sender
.send((
request.lines().next().unwrap().to_string(),
request[header_end..header_end + content_length].to_string(),
))
.unwrap();
let response_body = r#"{"documents":[{"databases":[{"name":"admin","sizeOnDisk":40960,"empty":false},{"name":"app","sizeOnDisk":8192,"empty":true}],"ok":1}]}"#;
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
response_body.len(),
response_body
)
.unwrap();
});
let backend = WebBackend::new(format!("http://{address}"), String::new()).unwrap();
backend.auth.lock().await.checked = true;
let connection = new_connection_config(
"legacy".to_string(),
"Legacy MongoDB".to_string(),
DatabaseType::MongoDb,
"localhost".to_string(),
27017,
String::new(),
String::new(),
Some("app".to_string()),
false,
Some("mongodb-legacy".to_string()),
)
.unwrap();
backend.connected.lock().await.insert(connection.id.clone(), connection.clone());
let result = backend.execute_mongo_command(&connection, "app", &MongoCommand::ShowDatabases).await.unwrap();
server.join().unwrap();
let (request_line, body) = request_receiver.recv().unwrap();
assert_eq!(request_line, "POST /api/mongo/run-command HTTP/1.1");
let request: Value = serde_json::from_str(&body).unwrap();
assert_eq!(request["connectionId"], "legacy");
assert_eq!(request["database"], "admin");
assert_eq!(request["commandJson"], r#"{"listDatabases":1}"#);
assert_eq!(result.columns, ["name", "sizeOnDisk", "empty"]);
assert_eq!(result.rows.len(), 2);
assert_eq!(result.affected_rows, 2);
}
#[tokio::test]
async fn web_mongo_find_explain_uses_explain_endpoint_and_preserves_options() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+61
View File
@@ -7,6 +7,7 @@ import {
mongoAggregateWriteStage,
mongoCollectionStatsToQueryResult,
mongoCountToQueryResult,
mongoDatabasesToQueryResult,
mongoDistinctToQueryResult,
mongoDocumentsToQueryResult,
mongoDroppedIndexesToQueryResult,
@@ -106,6 +107,66 @@ test("parseMongoRunCommand normalizes one non-empty command document", () => {
}
});
test("parseMongoCommand accepts read-only show-databases aliases exactly", () => {
for (const source of ["show dbs", "SHOW DATABASES;", "/* databases */ show dbs -- list"]) {
assert.deepEqual(parseMongoCommand(source)?.command, { kind: "showDatabases" }, source);
}
for (const source of ["show dbs extra", "show database", "show collections", "show"]) {
assert.equal(parseMongoCommand(source), null, source);
}
assert.equal(evaluateMongoWriteSafety(parseMongoCommand("db.runCommand({listDatabases: 1})")!.command as MongoWriteCommand, {}).allowed, false);
});
test("splitMongoCommands keeps show databases after a database switch", () => {
assert.deepEqual(
splitMongoCommands("use app\nshow databases").map(({ command }) => command.kind),
["use", "showDatabases"],
);
});
test("mongoDatabasesToQueryResult preserves metadata and bounds rows", () => {
assert.deepEqual(
mongoDatabasesToQueryResult(
[
{
databases: [
{ name: "admin", sizeOnDisk: 40960, empty: false },
{ name: "app", sizeOnDisk: { $numberLong: "8192" }, empty: true },
{ name: "logs", sizeOnDisk: 1024, empty: false },
],
totalSize: 50176,
ok: 1,
},
],
4.4,
2,
),
{
columns: ["name", "sizeOnDisk", "empty"],
rows: [
["admin", 40960, false],
["app", "8192", true],
],
affected_rows: 3,
execution_time_ms: 4,
truncated: true,
has_more: true,
},
);
assert.deepEqual(mongoDatabasesToQueryResult([{ databases: [] }], 0, 10), {
columns: ["name", "sizeOnDisk", "empty"],
rows: [],
affected_rows: 0,
execution_time_ms: 0,
truncated: false,
has_more: false,
});
assert.throws(() => mongoDatabasesToQueryResult([{ ok: 1 }], 0, 10), /databases array/);
});
test("runCommand support does not accept arbitrary Mongo shell JavaScript", () => {
assert.deepEqual(
splitMongoCommands(`for (let i = 0; i < 2; i += 1) {
+61
View File
@@ -5073,6 +5073,67 @@ test("mongo runCommand execution follows use and preserves document results", as
}
});
test("mongo show dbs executes one bounded read-only admin command", async () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
const connectionStore = useConnectionStore();
const store = useQueryStore();
const originalFetch = globalThis.fetch;
const runCommandBodies: any[] = [];
connectionStore.addEphemeralConnection({
...conn("mongo-1"),
db_type: "mongodb",
port: 27017,
});
globalThis.fetch = withConnectionHealthMock(async (input, init) => {
if (String(input) === "/api/mongo/run-command") {
runCommandBodies.push(JSON.parse(String(init?.body ?? "{}")));
return Response.json({
documents: [
{
databases: [
{ name: "admin", sizeOnDisk: 40960, empty: false },
{ name: "app", sizeOnDisk: 8192, empty: true },
],
totalSize: 49152,
ok: 1,
},
],
total: 1,
total_is_exact: true,
});
}
return new Response("unexpected request", { status: 500 });
});
try {
const tabId = store.createTab("mongo-1", "accounting", "Query", "query", "");
await store.executeTabSql(tabId, "show dbs");
const tab = store.tabs.find((item) => item.id === tabId);
assert.equal(runCommandBodies.length, 1);
assert.deepEqual(runCommandBodies[0], {
connectionId: "mongo-1",
database: "admin",
commandJson: '{"listDatabases":1}',
executionId: runCommandBodies[0].executionId,
});
assert.equal(typeof runCommandBodies[0].executionId, "string");
assert.deepEqual(tab?.result?.columns, ["name", "sizeOnDisk", "empty"]);
assert.deepEqual(tab?.result?.rows, [
["admin", 40960, false],
["app", 8192, true],
]);
assert.equal(tab?.result?.affected_rows, 2);
assert.equal(tab?.mongoEditTarget, undefined);
} finally {
globalThis.fetch = originalFetch;
restoreStorage();
}
});
test("mongo dropIndex execution uses the dedicated drop-indexes endpoint", async () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());