fix(mongodb): reject unsupported find() chain methods instead of dropping them
This commit is contained in:
1 parent
6d2c8a5edb
commit
58200ce5a4
2 files changed
+115
-2
No files matched your search
@@ -160,6 +160,13 @@ function diagnoseMongoCommand(source: string): string | null {
|
||||
}
|
||||
|
||||
const tail = source.slice(closeIndex + 1).trim();
|
||||
if (tail && method === "find") {
|
||||
const chainProblem = describeUnsupportedFindChain(tail);
|
||||
if (chainProblem) return chainProblem;
|
||||
}
|
||||
if (tail && (method === "findOne" || method === "countDocuments" || method === "count" || method === "distinct")) {
|
||||
return `${method}() returns a result, not a cursor, so nothing can be chained after it: "${tail.length > 40 ? `${tail.slice(0, 40)}…` : tail}".`;
|
||||
}
|
||||
if (tail) return `Unexpected text after ${method}(...): "${tail.length > 40 ? `${tail.slice(0, 40)}…` : tail}".`;
|
||||
if (method === "bulkWrite" && args[0]) {
|
||||
const operations = normalizeJsonArgument(args[0]);
|
||||
@@ -355,8 +362,11 @@ export function parseMongoFindCommand(input: string): MongoFindCommand | null {
|
||||
}
|
||||
|
||||
const chain = source.slice(findCloseIndex + 1).trim();
|
||||
if (chain && !chain.startsWith(".")) return null;
|
||||
if (findChainedMethodCallIndex(chain, "count") >= 0) return null;
|
||||
// Every chained call must be one the editor executes; dropping an unknown one
|
||||
// (`.hint()`, `.forEach()`, `.explain()`) would run a different query than written.
|
||||
const calls = listChainedCalls(chain);
|
||||
if (!calls || calls.some((call) => !FIND_CHAIN_METHODS.has(call.name) || (isNoopCursorMethod(call.name) && call.args.trim()))) return null;
|
||||
if (calls.some((call) => call.name === "count")) return null;
|
||||
|
||||
const sortArg = readChainedCallArgument(chain, "sort");
|
||||
let sort: string | undefined;
|
||||
@@ -1554,6 +1564,66 @@ function readChainedCallArgument(source: string, name: string): string | undefin
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Cursor methods the editor executes after `find()`. `toArray` / `pretty` change nothing and are dropped. */
|
||||
const FIND_CHAIN_METHODS = new Set(["sort", "skip", "limit", "collation", "count", "toArray", "pretty"]);
|
||||
|
||||
function isNoopCursorMethod(name: string): boolean {
|
||||
return name === "toArray" || name === "pretty";
|
||||
}
|
||||
|
||||
interface ChainedCall {
|
||||
name: string;
|
||||
/** Raw text between the parentheses. */
|
||||
args: string;
|
||||
/** Offset of the `.` within the chain text. */
|
||||
index: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenise `.name(args).name(args)…`. Returns null when the chain is not a sequence
|
||||
* of calls, e.g. a property access such as `.count` without parentheses or trailing text.
|
||||
*/
|
||||
export function listChainedCalls(chain: string): ChainedCall[] | null {
|
||||
const calls: ChainedCall[] = [];
|
||||
let index = 0;
|
||||
while (index < chain.length) {
|
||||
const rest = chain.slice(index);
|
||||
if (!rest.trim()) break;
|
||||
const match = /^\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/.exec(rest);
|
||||
if (!match) return null;
|
||||
const openIndex = index + match[0].length - 1;
|
||||
const closeIndex = findMatchingParen(chain, openIndex);
|
||||
if (closeIndex < 0) return null;
|
||||
calls.push({ name: match[1]!, args: chain.slice(openIndex + 1, closeIndex), index: index + match[0].indexOf(".") });
|
||||
index = closeIndex + 1;
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
/** Why a `find()` chain was rejected, for the parse-failure diagnostics. */
|
||||
function describeUnsupportedFindChain(chain: string): string | null {
|
||||
const calls = listChainedCalls(chain);
|
||||
if (!calls) return null;
|
||||
const offending = calls.find((call) => !FIND_CHAIN_METHODS.has(call.name) || (isNoopCursorMethod(call.name) && call.args.trim()));
|
||||
if (!offending) return null;
|
||||
const supported = "Supported after find(): sort, skip, limit, collation, count, toArray, pretty.";
|
||||
switch (offending.name) {
|
||||
case "explain":
|
||||
return `find().explain() is not supported in the editor yet; aggregate([...], { explain: true }) is. ${supported}`;
|
||||
case "itcount":
|
||||
case "size":
|
||||
return `find().${offending.name}() is not supported; use find().count() or countDocuments() to count results. ${supported}`;
|
||||
case "forEach":
|
||||
case "map":
|
||||
return `find().${offending.name}() runs JavaScript, which the editor does not execute; run the find and read the results instead.`;
|
||||
case "toArray":
|
||||
case "pretty":
|
||||
return `find().${offending.name}() takes no arguments.`;
|
||||
default:
|
||||
return `find().${offending.name}() is not supported yet. ${supported}`;
|
||||
}
|
||||
}
|
||||
|
||||
function hasSingleEmptyChainedCall(source: string, name: string): boolean {
|
||||
const trimmed = source.trim();
|
||||
const match = chainedMethodCallPattern(name).exec(trimmed);
|
||||
|
||||
Reference in new issue
Block a user