fix(mongodb): support compass shell commands
This commit is contained in:
1 parent
a0dcee51bc
commit
a87b8ece31
10 files changed
+512
-10
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user