feat(mongo): add dropIndex and dropIndexes support across full stack
This commit is contained in:
1 parent
bb72a58e54
commit
93eeeaa9c6
29 files changed
+1142
-24
No files matched your search
@@ -379,6 +379,7 @@ export const mongoFindDocuments = forward("mongoFindDocuments");
|
||||
export const mongoServerVersion = forward("mongoServerVersion");
|
||||
export const mongoAggregateDocuments = forward("mongoAggregateDocuments");
|
||||
export const mongoCreateIndex = forward("mongoCreateIndex");
|
||||
export const mongoDropIndexes = forward("mongoDropIndexes");
|
||||
export const documentInsertDocument = forward("documentInsertDocument");
|
||||
export const mongoInsertDocument = forward("mongoInsertDocument");
|
||||
export const mongoInsertDocuments = forward("mongoInsertDocuments");
|
||||
|
||||
@@ -1772,6 +1772,10 @@ export async function mongoCreateIndex(connectionId: string, database: string, c
|
||||
return post("/api/mongo/create-index", { connectionId, database, collection, keysJson, optionsJson });
|
||||
}
|
||||
|
||||
export async function mongoDropIndexes(connectionId: string, database: string, collection: string, indexesJson?: string, single = false): Promise<{ dropped_names: string[]; affected_rows: number }> {
|
||||
return post("/api/mongo/drop-indexes", { connectionId, database, collection, indexesJson, single });
|
||||
}
|
||||
|
||||
export async function mongoInsertDocument(connectionId: string, database: string, collection: string, docJson: string): Promise<string> {
|
||||
return documentInsertDocument(connectionId, database, collection, docJson);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ const COLLECTION_METHODS = [
|
||||
{ label: "deleteMany", detail: "Delete all matching documents", apply: "deleteMany({})" },
|
||||
{ label: "getIndexes", detail: "List collection indexes", apply: "getIndexes()" },
|
||||
{ label: "createIndex", detail: "Create an index", apply: "createIndex({ ${field}: 1 })" },
|
||||
{ label: "dropIndex", detail: "Drop one index", apply: 'dropIndex("${indexName}")' },
|
||||
{ label: "dropIndexes", detail: "Drop collection indexes", apply: "dropIndexes()" },
|
||||
] as const;
|
||||
|
||||
const CURSOR_METHODS = [
|
||||
@@ -66,7 +68,7 @@ const FIELD_SNIPPETS = [
|
||||
const QUERY_OPERATORS = ["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$nin", "$exists", "$regex", "$and", "$or", "$nor", "$not", "$elemMatch"];
|
||||
const UPDATE_OPERATORS = ["$set", "$unset", "$inc", "$push", "$pull", "$addToSet", "$rename", "$currentDate", "$setOnInsert"];
|
||||
const PIPELINE_STAGES = ["$match", "$project", "$group", "$sort", "$limit", "$skip", "$unwind", "$lookup", "$addFields", "$count", "$facet"];
|
||||
const QUERY_METHODS = ["find", "findOne", "countDocuments", "updateOne", "updateMany", "deleteOne", "deleteMany", "sort"];
|
||||
const QUERY_METHODS = ["find", "findOne", "countDocuments", "updateOne", "updateMany", "deleteOne", "deleteMany", "dropIndex", "dropIndexes", "sort"];
|
||||
|
||||
export function getMongoCompletionContext(text: string, cursor: number): MongoCompletionContext {
|
||||
const safeCursor = Math.max(0, Math.min(cursor, text.length));
|
||||
|
||||
@@ -12,7 +12,7 @@ interface FormatState {
|
||||
stack: Array<{ char: string; expanded: boolean; chainCall?: boolean }>;
|
||||
}
|
||||
|
||||
const CHAIN_METHODS = new Set(["find", "findOne", "aggregate", "countDocuments", "distinct", "insertOne", "insertMany", "updateOne", "updateMany", "deleteOne", "deleteMany", "getIndexes", "createIndex", "sort", "limit", "skip"]);
|
||||
const CHAIN_METHODS = new Set(["find", "findOne", "aggregate", "countDocuments", "distinct", "insertOne", "insertMany", "updateOne", "updateMany", "deleteOne", "deleteMany", "getIndexes", "createIndex", "dropIndex", "dropIndexes", "sort", "limit", "skip"]);
|
||||
|
||||
export function formatMongoShellText(text: string, settings: Partial<SqlFormatterSettings> = DEFAULT_SQL_FORMATTER_SETTINGS): string {
|
||||
if (!text.trim()) return text;
|
||||
|
||||
@@ -35,7 +35,9 @@ 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 }
|
||||
| { kind: "createIndex"; collection: string; keys: string; options?: string };
|
||||
| { kind: "createIndex"; collection: string; keys: string; options?: string }
|
||||
| { kind: "dropIndex"; collection: string; index: string }
|
||||
| { kind: "dropIndexes"; collection: string; indexes?: string };
|
||||
|
||||
export interface MongoAggregateSafetyOptions {
|
||||
allowWrites?: boolean;
|
||||
@@ -220,9 +222,47 @@ export function parseMongoWriteCommand(input: string): MongoWriteCommand | null
|
||||
return { kind: "createIndex", collection: createIndex.collection, keys, ...(options ? { options } : {}) };
|
||||
}
|
||||
|
||||
const dropIndex = parseCollectionMethodTarget(source, "dropIndex");
|
||||
if (dropIndex) {
|
||||
const args = parseMethodArgs(source, dropIndex.methodCallIndex);
|
||||
if (!args) return null;
|
||||
const index = parseMongoDropIndexArgument(args);
|
||||
return index ? { kind: "dropIndex", collection: dropIndex.collection, index } : null;
|
||||
}
|
||||
|
||||
const dropIndexes = parseCollectionMethodTarget(source, "dropIndexes");
|
||||
if (dropIndexes) {
|
||||
const args = parseMethodArgs(source, dropIndexes.methodCallIndex);
|
||||
if (!args) return null;
|
||||
const indexes = parseMongoDropIndexesArgument(args);
|
||||
return indexes !== null ? { kind: "dropIndexes", collection: dropIndexes.collection, ...(indexes ? { indexes } : {}) } : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: MongoAggregateSafetyOptions): { allowed: boolean; reason?: string } {
|
||||
if (!options.allowWrites) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "MCP MongoDB execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write commands.",
|
||||
};
|
||||
}
|
||||
if (!options.allowDangerous && (command.kind === "update" || command.kind === "delete") && isEmptyJsonObject(command.filter)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "MongoDB update/delete commands must include a non-empty filter unless DBX_MCP_ALLOW_DANGEROUS_SQL=1 is set.",
|
||||
};
|
||||
}
|
||||
if (!options.allowDangerous && mongoDropIndexesRequiresDangerous(command)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "MongoDB dropIndexes() without a specific single index requires DBX_MCP_ALLOW_DANGEROUS_SQL=1.",
|
||||
};
|
||||
}
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
export function mongoAggregateWriteStage(pipelineJson: string): "$out" | "$merge" | null {
|
||||
try {
|
||||
const pipeline = JSON.parse(pipelineJson);
|
||||
@@ -310,6 +350,15 @@ export function mongoCreateIndexToQueryResult(name: string, executionTimeMs: num
|
||||
};
|
||||
}
|
||||
|
||||
export function mongoDroppedIndexesToQueryResult(names: string[], executionTimeMs: number): QueryResult {
|
||||
return {
|
||||
columns: ["name"],
|
||||
rows: names.map((name) => [name]),
|
||||
affected_rows: names.length,
|
||||
execution_time_ms: Math.max(0, Math.round(executionTimeMs)),
|
||||
};
|
||||
}
|
||||
|
||||
export function mongoUseToQueryResult(database: string, executionTimeMs: number): QueryResult {
|
||||
return {
|
||||
columns: ["message"],
|
||||
@@ -444,6 +493,26 @@ function convertSingleQuotedStrings(source: string): string {
|
||||
return quote === "'" ? source : result + source.slice(copiedUntil);
|
||||
}
|
||||
|
||||
function parseMongoDropIndexArgument(args: string[]): string | null {
|
||||
if (args.length !== 1 || !args[0]?.trim()) return null;
|
||||
const normalized = normalizeJsonArgument(args[0]);
|
||||
if (!normalized) return null;
|
||||
const parsed = parseNormalizedJson(normalized);
|
||||
if (typeof parsed === "string") return parsed === "*" ? null : normalized;
|
||||
return isNonEmptyRecord(parsed) ? normalized : null;
|
||||
}
|
||||
|
||||
function parseMongoDropIndexesArgument(args: string[]): string | undefined | null {
|
||||
if (args.length !== 1) return null;
|
||||
if (!args[0]?.trim()) return undefined;
|
||||
const normalized = normalizeJsonArgument(args[0]);
|
||||
if (!normalized) return null;
|
||||
const parsed = parseNormalizedJson(normalized);
|
||||
if (typeof parsed === "string") return normalized;
|
||||
if (isNonEmptyRecord(parsed)) return normalized;
|
||||
return Array.isArray(parsed) && parsed.length > 0 && parsed.every((item) => typeof item === "string") ? normalized : null;
|
||||
}
|
||||
|
||||
export function quoteUnquotedObjectKeys(source: string): string {
|
||||
let result = "";
|
||||
let quote: string | null = null;
|
||||
@@ -577,6 +646,31 @@ function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function parseNormalizedJson(json: string): unknown {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isNonEmptyRecord(value: unknown): value is Record<string, unknown> {
|
||||
return isRecord(value) && Object.keys(value).length > 0;
|
||||
}
|
||||
|
||||
function isEmptyJsonObject(json: string): boolean {
|
||||
const parsed = parseNormalizedJson(json);
|
||||
return isRecord(parsed) && Object.keys(parsed).length === 0;
|
||||
}
|
||||
|
||||
function mongoDropIndexesRequiresDangerous(command: MongoWriteCommand): boolean {
|
||||
if (command.kind !== "dropIndexes") return false;
|
||||
if (!command.indexes) return true;
|
||||
const parsed = parseNormalizedJson(command.indexes);
|
||||
if (parsed === "*") return true;
|
||||
return Array.isArray(parsed) && parsed.length > 1;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
@@ -1504,6 +1504,10 @@ export async function mongoCreateIndex(connectionId: string, database: string, c
|
||||
return invoke("mongo_create_index", { connectionId, database, collection, keysJson, optionsJson });
|
||||
}
|
||||
|
||||
export async function mongoDropIndexes(connectionId: string, database: string, collection: string, indexesJson?: string, single = false): Promise<{ dropped_names: string[]; affected_rows: number }> {
|
||||
return invoke("mongo_drop_indexes", { connectionId, database, collection, indexesJson, single });
|
||||
}
|
||||
|
||||
export async function mongoInsertDocument(connectionId: string, database: string, collection: string, docJson: string): Promise<string> {
|
||||
return documentInsertDocument(connectionId, database, collection, docJson);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user