fix(mongodb): guard aggregate execution paths

This commit is contained in:
徐文彬
2026-05-28 21:29:08 +08:00
parent c56bcb8e2d
commit 5ea146e08e
19 changed files with 166 additions and 26 deletions
+1 -1
View File
@@ -622,7 +622,7 @@ function ensureQueryTab(): string {
const tab = activeTab.value;
if (tab && tab.mode === "query") return tab.id;
const connId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id || "";
const db = tab?.database || connectionStore.getConfig(connId)?.database || "";
const db = tab?.connectionId === connId ? tab.database : connectionStore.getConfig(connId)?.database || "";
return queryStore.createTab(connId, db, undefined, "query");
}
+11 -3
View File
@@ -54,9 +54,15 @@ export function useTauriEvents(deps: {
}
}).then((unlisten) => unlistenHandles.push(unlisten));
listen<{ connection_id: string; database: string; sql: string }>("mcp-execute-query", async (event) => {
listen<{
connection_id: string;
database: string;
sql: string;
allow_writes?: boolean;
allow_dangerous?: boolean;
}>("mcp-execute-query", async (event) => {
try {
const { connection_id, database, sql } = event.payload;
const { connection_id, database, sql, allow_writes, allow_dangerous } = event.payload;
if (!connectionStore.connections.length) await connectionStore.initFromDisk();
const config = connectionStore.getConfig(connection_id);
if (!config) return;
@@ -64,7 +70,9 @@ export function useTauriEvents(deps: {
await connectionStore.ensureConnected(connection_id);
const tabId = queryStore.createTab(connection_id, database, undefined, "query");
queryStore.updateSql(tabId, sql);
await queryStore.executeTabSql(tabId, sql);
await queryStore.executeTabSql(tabId, sql, {
mongoSafety: { allowWrites: !!allow_writes, allowDangerous: !!allow_dangerous },
});
focusCurrentWindow();
} catch (e) {
console.error("[DBX] mcp-execute-query error:", e);
+2 -1
View File
@@ -1272,8 +1272,9 @@ export async function mongoAggregateDocuments(
database: string,
collection: string,
pipelineJson: string,
maxRows?: number,
): Promise<MongoDocumentResult> {
return post("/api/mongo/aggregate-documents", { connectionId, database, collection, pipelineJson });
return post("/api/mongo/aggregate-documents", { connectionId, database, collection, pipelineJson, maxRows });
}
export async function mongoInsertDocument(
+41
View File
@@ -18,6 +18,11 @@ export interface MongoAggregateCommand {
pipeline: string;
}
export interface MongoAggregateSafetyOptions {
allowWrites?: boolean;
allowDangerous?: boolean;
}
const DEFAULT_LIMIT = 100;
export function parseMongoFindCommand(input: string): MongoFindCommand | null {
@@ -102,6 +107,42 @@ export function parseMongoAggregateCommand(input: string): MongoAggregateCommand
};
}
export function mongoAggregateWriteStage(pipelineJson: string): "$out" | "$merge" | null {
try {
const pipeline = JSON.parse(pipelineJson);
if (!Array.isArray(pipeline)) return null;
for (const stage of pipeline) {
if (!isRecord(stage)) continue;
if (Object.prototype.hasOwnProperty.call(stage, "$out")) return "$out";
if (Object.prototype.hasOwnProperty.call(stage, "$merge")) return "$merge";
}
} catch {
return null;
}
return null;
}
export function evaluateMongoAggregateSafety(
command: MongoAggregateCommand,
options: MongoAggregateSafetyOptions,
): { allowed: boolean; reason?: string } {
const writeStage = mongoAggregateWriteStage(command.pipeline);
if (!writeStage) return { allowed: true };
if (!options.allowWrites) {
return {
allowed: false,
reason: `MongoDB aggregate stage "${writeStage}" writes data. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.`,
};
}
if (!options.allowDangerous) {
return {
allowed: false,
reason: `MongoDB aggregate stage "${writeStage}" is dangerous. Set DBX_MCP_ALLOW_DANGEROUS_SQL=1 to allow it.`,
};
}
return { allowed: true };
}
export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeMs: number, total: number): QueryResult {
const columns: string[] = [];
+2 -1
View File
@@ -1103,8 +1103,9 @@ export async function mongoAggregateDocuments(
database: string,
collection: string,
pipelineJson: string,
maxRows?: number,
): Promise<MongoDocumentResult> {
return invoke("mongo_aggregate_documents", { connectionId, database, collection, pipelineJson });
return invoke("mongo_aggregate_documents", { connectionId, database, collection, pipelineJson, maxRows });
}
export async function mongoInsertDocument(
+8
View File
@@ -10,11 +10,13 @@ import { buildExplainSql, parseExplainResult } from "@/lib/explainPlan";
import { allEditableColumnsWriteable, allPrimaryKeysPresent, sourceColumnsForResult } from "@/lib/sqlAnalysis";
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
import {
evaluateMongoAggregateSafety,
mongoCountToQueryResult,
mongoDocumentsToQueryResult,
parseMongoAggregateCommand,
parseMongoCountDocumentsCommand,
parseMongoFindCommand,
type MongoAggregateSafetyOptions,
} from "@/lib/mongoShellCommand";
import { AGENT_DRIVER_TYPES } from "@/lib/databaseCapabilities";
import { editablePrimaryKeys } from "@/lib/tableEditing";
@@ -528,6 +530,7 @@ export const useQueryStore = defineStore("query", () => {
resultBaseSql?: string;
resultSortedSql?: string | undefined;
pagination?: { limit: number; offset: number; sessionId?: string };
mongoSafety?: MongoAggregateSafetyOptions;
},
) {
const tab = tabs.value.find((t) => t.id === id);
@@ -655,6 +658,10 @@ export const useQueryStore = defineStore("query", () => {
const mongoAggregate = conn?.db_type === "mongodb" ? parseMongoAggregateCommand(sql) : null;
if (mongoAggregate) {
if (options?.mongoSafety) {
const safety = evaluateMongoAggregateSafety(mongoAggregate, options.mongoSafety);
if (!safety.allowed) throw new Error(safety.reason);
}
await connStore.ensureConnected(tab.connectionId);
console.info("[DBX][executeTabSql:mongo-aggregate:start]", { traceId, collection: mongoAggregate.collection });
const result = await api.mongoAggregateDocuments(
@@ -662,6 +669,7 @@ export const useQueryStore = defineStore("query", () => {
tab.database,
mongoAggregate.collection,
mongoAggregate.pipeline,
pageLimit,
);
console.info("[DBX][executeTabSql:mongo-aggregate:done]", {
traceId,