fix(mongodb): support compass shell commands

This commit is contained in:
t8y2 committed 2026-06-01 16:24:31 +08:00
1 parent a0dcee51bc
commit a87b8ece31
10 files changed
+512 -10

No files matched your search

+3
View File
@@ -214,8 +214,11 @@ export const mongoListCollections = forward("mongoListCollections");
export const mongoFindDocuments = forward("mongoFindDocuments");
export const mongoAggregateDocuments = forward("mongoAggregateDocuments");
export const mongoInsertDocument = forward("mongoInsertDocument");
export const mongoInsertDocuments = forward("mongoInsertDocuments");
export const mongoUpdateDocument = forward("mongoUpdateDocument");
export const mongoUpdateDocuments = forward("mongoUpdateDocuments");
export const mongoDeleteDocument = forward("mongoDeleteDocument");
export const mongoDeleteDocuments = forward("mongoDeleteDocuments");
// History
export const saveHistory = forward("saveHistory");
+30
View File
@@ -1304,6 +1304,15 @@ export async function mongoInsertDocument(
return post("/api/mongo/insert-document", { connectionId, database, collection, docJson });
}
export async function mongoInsertDocuments(
connectionId: string,
database: string,
collection: string,
docsJson: string,
): Promise<{ affected_rows: number }> {
return post("/api/mongo/insert-documents", { connectionId, database, collection, docsJson });
}
export async function mongoUpdateDocument(
connectionId: string,
database: string,
@@ -1314,6 +1323,17 @@ export async function mongoUpdateDocument(
return post("/api/mongo/update-document", { connectionId, database, collection, id, docJson });
}
export async function mongoUpdateDocuments(
connectionId: string,
database: string,
collection: string,
filterJson: string,
updateJson: string,
many: boolean,
): Promise<{ affected_rows: number }> {
return post("/api/mongo/update-documents", { connectionId, database, collection, filterJson, updateJson, many });
}
export async function mongoDeleteDocument(
connectionId: string,
database: string,
@@ -1323,6 +1343,16 @@ export async function mongoDeleteDocument(
return post("/api/mongo/delete-document", { connectionId, database, collection, id });
}
export async function mongoDeleteDocuments(
connectionId: string,
database: string,
collection: string,
filterJson: string,
many: boolean,
): Promise<{ affected_rows: number }> {
return post("/api/mongo/delete-documents", { connectionId, database, collection, filterJson, many });
}
// ---------------------------------------------------------------------------
// History
// ---------------------------------------------------------------------------
+161 -1
View File
@@ -18,6 +18,11 @@ export interface MongoAggregateCommand {
pipeline: string;
}
export type MongoWriteCommand =
| { kind: "insert"; collection: string; docsJson: string }
| { kind: "update"; collection: string; filter: string; update: string; many: boolean }
| { kind: "delete"; collection: string; filter: string; many: boolean };
export interface MongoAggregateSafetyOptions {
allowWrites?: boolean;
allowDangerous?: boolean;
@@ -107,6 +112,51 @@ export function parseMongoAggregateCommand(input: string): MongoAggregateCommand
};
}
export function parseMongoWriteCommand(input: string): MongoWriteCommand | null {
const source = input.trim().replace(/;$/, "").trim();
const insertOne = parseCollectionMethodTarget(source, "insertOne");
if (insertOne) {
const args = parseMethodArgs(source, insertOne.methodCallIndex);
if (!args || args.length !== 1) return null;
const doc = normalizeJsonArgument(args[0]);
return doc ? { kind: "insert", collection: insertOne.collection, docsJson: doc } : null;
}
const insertMany = parseCollectionMethodTarget(source, "insertMany");
if (insertMany) {
const args = parseMethodArgs(source, insertMany.methodCallIndex);
if (!args || args.length !== 1) return null;
const docs = normalizeJsonArgument(args[0]);
if (!docs) return null;
return Array.isArray(JSON.parse(docs))
? { kind: "insert", collection: insertMany.collection, docsJson: docs }
: null;
}
for (const method of ["updateOne", "updateMany"] as const) {
const target = parseCollectionMethodTarget(source, method);
if (!target) continue;
const args = parseMethodArgs(source, target.methodCallIndex);
if (!args || args.length !== 2) return null;
const filter = normalizeJsonArgument(args[0]);
const update = normalizeJsonArgument(args[1]);
if (!filter || !update) return null;
return { kind: "update", collection: target.collection, filter, update, many: method === "updateMany" };
}
for (const method of ["deleteOne", "deleteMany"] as const) {
const target = parseCollectionMethodTarget(source, method);
if (!target) continue;
const args = parseMethodArgs(source, target.methodCallIndex);
if (!args || args.length !== 1) return null;
const filter = normalizeJsonArgument(args[0]);
if (!filter) return null;
return { kind: "delete", collection: target.collection, filter, many: method === "deleteMany" };
}
return null;
}
export function mongoAggregateWriteStage(pipelineJson: string): "$out" | "$merge" | null {
try {
const pipeline = JSON.parse(pipelineJson);
@@ -179,6 +229,15 @@ export function mongoCountToQueryResult(total: number, executionTimeMs: number):
};
}
export function mongoWriteToQueryResult(affectedRows: number, executionTimeMs: number): QueryResult {
return {
columns: [],
rows: [],
affected_rows: affectedRows,
execution_time_ms: Math.max(0, Math.round(executionTimeMs)),
};
}
function parseFindTarget(source: string): { collection: string; findCallIndex: number } | null {
const direct = parseCollectionMethodTarget(source, "find");
if (direct) {
@@ -217,7 +276,9 @@ function parseCollectionMethodTarget(
function normalizeJsonArgument(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return "{}";
const preprocessed = trimmed.replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}');
const preprocessed = quoteUnquotedObjectKeys(
convertSingleQuotedStrings(trimmed.replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}')),
);
try {
JSON.parse(preprocessed);
return preprocessed;
@@ -226,6 +287,105 @@ function normalizeJsonArgument(value: string): string | null {
}
}
function parseMethodArgs(source: string, methodCallIndex: number): string[] | null {
const openIndex = source.indexOf("(", methodCallIndex);
const closeIndex = findMatchingParen(source, openIndex);
if (closeIndex < 0 || source.slice(closeIndex + 1).trim()) return null;
return splitTopLevel(source.slice(openIndex + 1, closeIndex));
}
function convertSingleQuotedStrings(source: string): string {
let result = "";
let copiedUntil = 0;
let quote: string | null = null;
let start = 0;
let value = "";
let escaped = false;
for (let i = 0; i < source.length; i += 1) {
const char = source[i];
if (!quote) {
if (char === "'") {
quote = char;
start = i;
value = "";
escaped = false;
} else if (char === '"') {
quote = char;
}
continue;
}
if (quote === '"') {
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === '"') quote = null;
continue;
}
if (escaped) {
value += char;
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === "'") {
result += source.slice(copiedUntil, start) + JSON.stringify(value);
copiedUntil = i + 1;
quote = null;
} else {
value += char;
}
}
return quote === "'" ? source : result + source.slice(copiedUntil);
}
function quoteUnquotedObjectKeys(source: string): string {
let result = "";
let quote: string | null = null;
let escaped = false;
for (let i = 0; i < source.length; i += 1) {
const char = source[i];
if (quote) {
result += char;
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === quote) quote = null;
continue;
}
if (char === '"' || char === "'") {
quote = char;
result += char;
continue;
}
if (/[A-Za-z_$]/.test(char) && shouldQuoteObjectKey(source, i)) {
let end = i + 1;
while (/[\w$]/.test(source[end] || "")) end += 1;
result += `"${source.slice(i, end)}"`;
i = end - 1;
continue;
}
result += char;
}
return result;
}
function shouldQuoteObjectKey(source: string, index: number): boolean {
let before = index - 1;
while (/\s/.test(source[before] || "")) before -= 1;
if (source[before] !== "{" && source[before] !== ",") return false;
let after = index + 1;
while (/[\w$]/.test(source[after] || "")) after += 1;
while (/\s/.test(source[after] || "")) after += 1;
return source[after] === ":";
}
function readChainedIntegerArgument(source: string, name: string, fallback: number): number | null {
const raw = readChainedCallArgument(source, name);
if (raw === undefined) return fallback;
+46
View File
@@ -1141,6 +1141,16 @@ export async function mongoInsertDocument(
return invoke("mongo_insert_document", { connectionId, database, collection, docJson });
}
export async function mongoInsertDocuments(
connectionId: string,
database: string,
collection: string,
docsJson: string,
): Promise<{ affected_rows: number }> {
const affectedRows = await invoke<number>("mongo_insert_documents", { connectionId, database, collection, docsJson });
return { affected_rows: affectedRows };
}
export async function mongoUpdateDocument(
connectionId: string,
database: string,
@@ -1151,6 +1161,25 @@ export async function mongoUpdateDocument(
return invoke("mongo_update_document", { connectionId, database, collection, id, docJson });
}
export async function mongoUpdateDocuments(
connectionId: string,
database: string,
collection: string,
filterJson: string,
updateJson: string,
many: boolean,
): Promise<{ affected_rows: number }> {
const affectedRows = await invoke<number>("mongo_update_documents", {
connectionId,
database,
collection,
filterJson,
updateJson,
many,
});
return { affected_rows: affectedRows };
}
export async function mongoDeleteDocument(
connectionId: string,
database: string,
@@ -1160,6 +1189,23 @@ export async function mongoDeleteDocument(
return invoke("mongo_delete_document", { connectionId, database, collection, id });
}
export async function mongoDeleteDocuments(
connectionId: string,
database: string,
collection: string,
filterJson: string,
many: boolean,
): Promise<{ affected_rows: number }> {
const affectedRows = await invoke<number>("mongo_delete_documents", {
connectionId,
database,
collection,
filterJson,
many,
});
return { affected_rows: affectedRows };
}
// --- History ---
export interface HistoryEntry {
id: string;
+60
View File
@@ -13,9 +13,11 @@ import {
evaluateMongoAggregateSafety,
mongoCountToQueryResult,
mongoDocumentsToQueryResult,
mongoWriteToQueryResult,
parseMongoAggregateCommand,
parseMongoCountDocumentsCommand,
parseMongoFindCommand,
parseMongoWriteCommand,
type MongoAggregateSafetyOptions,
} from "@/lib/mongoShellCommand";
import { AGENT_DRIVER_TYPES } from "@/lib/databaseCapabilities";
@@ -797,6 +799,64 @@ export const useQueryStore = defineStore("query", () => {
return;
}
const mongoWrite = conn?.db_type === "mongodb" ? parseMongoWriteCommand(sql) : null;
if (mongoWrite) {
await connStore.ensureConnected(tab.connectionId);
console.info("[DBX][executeTabSql:mongo-write:start]", {
traceId,
kind: mongoWrite.kind,
collection: mongoWrite.collection,
});
let affectedRows = 0;
if (mongoWrite.kind === "insert") {
const result = await api.mongoInsertDocuments(
tab.connectionId,
tab.database,
mongoWrite.collection,
mongoWrite.docsJson,
);
affectedRows = result.affected_rows;
} else if (mongoWrite.kind === "update") {
const result = await api.mongoUpdateDocuments(
tab.connectionId,
tab.database,
mongoWrite.collection,
mongoWrite.filter,
mongoWrite.update,
mongoWrite.many,
);
affectedRows = result.affected_rows;
} else {
const result = await api.mongoDeleteDocuments(
tab.connectionId,
tab.database,
mongoWrite.collection,
mongoWrite.filter,
mongoWrite.many,
);
affectedRows = result.affected_rows;
}
console.info("[DBX][executeTabSql:mongo-write:done]", {
traceId,
affectedRows,
elapsed: elapsed(),
});
const current = tabs.value.find((t) => t.id === id);
if (current?.executionId === executionId) {
current.results = undefined;
current.activeResultIndex = undefined;
current.result = mongoWriteToQueryResult(affectedRows, performance.now() - startedAt);
touchResult(current);
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
}
return;
}
console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() });
const clientSessionId = tab.mode === "query" ? tab.id : undefined;
const executionOptions = {