From bc49bbbbfa4d6314a07caeee124ebc220eb2d620 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 24 Jun 2026 23:21:06 +0800 Subject: [PATCH] fix(mongo): parse newline chained find calls --- apps/desktop/src/lib/mongoShellCommand.ts | 35 ++++++++++---------- packages/app-tests/mongoShellCommand.test.ts | 16 +++++++++ packages/node-core/src/database.ts | 26 +++++++++++---- packages/node-core/tests/mongo-query.test.ts | 16 +++++++++ 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src/lib/mongoShellCommand.ts b/apps/desktop/src/lib/mongoShellCommand.ts index 7d6bc3497..b867ed478 100644 --- a/apps/desktop/src/lib/mongoShellCommand.ts +++ b/apps/desktop/src/lib/mongoShellCommand.ts @@ -305,19 +305,19 @@ function parseFindTarget(source: string): { collection: string; findCallIndex: n function parseCollectionMethodTarget(source: string, method: string): { collection: string; methodCallIndex: number } | null { const escapedMethod = escapeRegExp(method); - const direct = new RegExp(`^db\\.([A-Za-z_$][\\w$]*)\\.${escapedMethod}\\s*\\(`).exec(source); + const direct = new RegExp(`^db\\s*\\.\\s*([A-Za-z_$][\\w$]*)\\s*\\.\\s*${escapedMethod}\\s*\\(`).exec(source); if (direct) { return { collection: direct[1], - methodCallIndex: source.indexOf(`.${method}`, direct[0].length - `.${method}(`.length), + methodCallIndex: findChainedMethodCallIndex(source, method), }; } - const getCollection = new RegExp(`^db\\.getCollection\\s*\\(\\s*(["'])(.*?)\\1\\s*\\)\\.${escapedMethod}\\s*\\(`).exec(source); + const getCollection = new RegExp(`^db\\s*\\.\\s*getCollection\\s*\\(\\s*(["'])(.*?)\\1\\s*\\)\\s*\\.\\s*${escapedMethod}\\s*\\(`).exec(source); if (getCollection) { return { collection: getCollection[2], - methodCallIndex: source.indexOf(`.${method}`, getCollection[0].length - `.${method}(`.length), + methodCallIndex: findChainedMethodCallIndex(source, method), }; } @@ -444,24 +444,23 @@ function readChainedIntegerArgument(source: string, name: string, fallback: numb } function readChainedCallArgument(source: string, name: string): string | undefined { - const call = `.${name}`; - let index = source.indexOf(call); - while (index >= 0) { - const afterName = index + call.length; - const openIndex = skipWhitespace(source, afterName); - if (source[openIndex] === "(") { - const closeIndex = findMatchingParen(source, openIndex); - if (closeIndex >= 0) return source.slice(openIndex + 1, closeIndex); - } - index = source.indexOf(call, afterName); + const pattern = chainedMethodCallPattern(name); + let match = pattern.exec(source); + while (match) { + const openIndex = source.indexOf("(", match.index); + const closeIndex = findMatchingParen(source, openIndex); + if (closeIndex >= 0) return source.slice(openIndex + 1, closeIndex); + match = pattern.exec(source); } return undefined; } -function skipWhitespace(source: string, index: number) { - let cursor = index; - while (/\s/.test(source[cursor] || "")) cursor += 1; - return cursor; +function findChainedMethodCallIndex(source: string, name: string): number { + return chainedMethodCallPattern(name).exec(source)?.index ?? -1; +} + +function chainedMethodCallPattern(name: string): RegExp { + return new RegExp(`\\.\\s*${escapeRegExp(name)}\\s*\\(`, "g"); } function splitTopLevel(source: string): string[] { diff --git a/packages/app-tests/mongoShellCommand.test.ts b/packages/app-tests/mongoShellCommand.test.ts index 5d8644544..a5347df34 100644 --- a/packages/app-tests/mongoShellCommand.test.ts +++ b/packages/app-tests/mongoShellCommand.test.ts @@ -33,6 +33,22 @@ test("parseMongoFindCommand parses getCollection find with chained sort skip and }); }); +test("parseMongoFindCommand accepts line breaks before find and chained calls", () => { + const command = parseMongoFindCommand(`db.getCollection("accounting_reconciliations") +.find({ + "_id": ObjectId("68ad51ca84c8127bc7d44cb3") +}) +.sort({ lineNo: -1 }) +.skip(5) +.limit(20)`); + assert.ok(command); + assert.equal(command.collection, "accounting_reconciliations"); + assert.deepEqual(JSON.parse(command.filter), { _id: { $oid: "68ad51ca84c8127bc7d44cb3" } }); + assert.deepEqual(JSON.parse(command.sort || "{}"), { lineNo: -1 }); + assert.equal(command.skip, 5); + assert.equal(command.limit, 20); +}); + test("parseMongoFindCommand accepts Compass-style unquoted keys and ObjectId", () => { const command = parseMongoFindCommand("db.products.find({_id: ObjectId('6a045a92d2971e44243771a1')}).limit(1)"); assert.ok(command); diff --git a/packages/node-core/src/database.ts b/packages/node-core/src/database.ts index a23ff0946..6bccf6840 100644 --- a/packages/node-core/src/database.ts +++ b/packages/node-core/src/database.ts @@ -921,10 +921,11 @@ export function evaluateMongoAggregateSafety(command: MongoAggregateCommand, opt } function parseCollectionMethodTarget(source: string, method: string): { collection: string; methodCallIndex: number } | null { - const direct = new RegExp(`^db\\.([A-Za-z_$][\\w$]*)\\.${method}\\s*\\(`).exec(source); - if (direct) return { collection: direct[1], methodCallIndex: source.indexOf(`.${method}`) }; - const quoted = new RegExp(`^db\\.getCollection\\(\\s*(['"])([^'"]+)\\1\\s*\\)\\.${method}\\s*\\(`).exec(source); - if (quoted) return { collection: quoted[2], methodCallIndex: source.indexOf(`.${method}`) }; + const escapedMethod = escapeRegExp(method); + const direct = new RegExp(`^db\\s*\\.\\s*([A-Za-z_$][\\w$]*)\\s*\\.\\s*${escapedMethod}\\s*\\(`).exec(source); + if (direct) return { collection: direct[1], methodCallIndex: findChainedMethodCallIndex(source, method) }; + const quoted = new RegExp(`^db\\s*\\.\\s*getCollection\\s*\\(\\s*(['"])([^'"]+)\\1\\s*\\)\\s*\\.\\s*${escapedMethod}\\s*\\(`).exec(source); + if (quoted) return { collection: quoted[2], methodCallIndex: findChainedMethodCallIndex(source, method) }; return null; } @@ -936,14 +937,25 @@ function parseMethodArgs(source: string, methodCallIndex: number): string[] | nu } function readChainedCallArgument(chain: string, method: string): string | undefined { - const pattern = new RegExp(`\\.${method}\\s*\\(`, "g"); - const match = pattern.exec(chain); + const match = chainedMethodCallPattern(method).exec(chain); if (!match) return undefined; - const openIndex = match.index + match[0].lastIndexOf("("); + const openIndex = chain.indexOf("(", match.index); const closeIndex = findMatchingParen(chain, openIndex); return closeIndex < 0 ? undefined : chain.slice(openIndex + 1, closeIndex); } +function findChainedMethodCallIndex(source: string, method: string): number { + return chainedMethodCallPattern(method).exec(source)?.index ?? -1; +} + +function chainedMethodCallPattern(method: string): RegExp { + return new RegExp(`\\.\\s*${escapeRegExp(method)}\\s*\\(`, "g"); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function readChainedIntegerArgument(chain: string, method: string, fallback: number): number | null { const arg = readChainedCallArgument(chain, method); if (arg === undefined) return fallback; diff --git a/packages/node-core/tests/mongo-query.test.ts b/packages/node-core/tests/mongo-query.test.ts index 4a9cd59a8..2f87a2c26 100644 --- a/packages/node-core/tests/mongo-query.test.ts +++ b/packages/node-core/tests/mongo-query.test.ts @@ -12,6 +12,22 @@ test("parseMongoFindCommand accepts shell-style find commands", () => { }); }); +test("parseMongoFindCommand accepts line breaks before find and chained calls", () => { + const command = parseMongoFindCommand(`db.getCollection("operation_logs") +.find({ + "_id": ObjectId("68ad51ca84c8127bc7d44cb3") +}) +.sort({ ts: -1 }) +.skip(5) +.limit(10)`); + assert.ok(command); + assert.equal(command.collection, "operation_logs"); + assert.deepEqual(JSON.parse(command.filter), { _id: { $oid: "68ad51ca84c8127bc7d44cb3" } }); + assert.deepEqual(JSON.parse(command.sort || "{}"), { ts: -1 }); + assert.equal(command.skip, 5); + assert.equal(command.limit, 10); +}); + test("parseMongoFindCommand accepts Compass-style unquoted keys and ObjectId", () => { const command = parseMongoFindCommand("db.products.find({_id: ObjectId('6a045a92d2971e44243771a1')}).limit(1)"); assert.ok(command);