mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-30 00:19:35 +00:00
fix(mongodb): guard aggregate execution paths
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user