fix(mongo): accept db["collection"] bracket accessors

This commit is contained in:
Loc Nguyen authored and GitHub committed 2026-09-11 10:50:44 +08:00
1 parent bad696ed11
commit e1e925fbab
3 files changed
+110

No files matched your search

+82
View File
@@ -734,6 +734,21 @@ fn trim_mongo_outer_comments(mut source: &str) -> &str {
} }
fn parse_collection_prefix(source: &str) -> Result<(String, usize), String> { fn parse_collection_prefix(source: &str) -> Result<(String, usize), String> {
// db["orders-2024"] reaches names that are not valid identifiers, the same way the shell does.
if source.get(..3).is_some_and(|prefix| prefix.eq_ignore_ascii_case("db[")) {
let close = matching_bracket(source, 2).ok_or("Invalid db[\"collection\"] accessor.")?;
let collection = parse_string_arg(source[3..close].trim())?;
if collection.is_empty() {
return Err("Invalid MongoDB collection name.".to_string());
}
let end = close + 1;
let suffix = &source[end..];
let trimmed = suffix.trim_start();
if !trimmed.starts_with('.') {
return Err("MongoDB collection method is required.".to_string());
}
return Ok((collection, end + suffix.len() - trimmed.len()));
}
if !source.get(..3).is_some_and(|prefix| prefix.eq_ignore_ascii_case("db.")) { if !source.get(..3).is_some_and(|prefix| prefix.eq_ignore_ascii_case("db.")) {
return Err("MongoDB command must start with db.<collection>.".to_string()); return Err("MongoDB command must start with db.<collection>.".to_string());
} }
@@ -1258,6 +1273,43 @@ fn matching_paren(source: &str, open: usize) -> Option<usize> {
None None
} }
/// Offset of the `]` closing the bracket at `open`, ignoring brackets inside strings.
fn matching_bracket(source: &str, open: usize) -> Option<usize> {
let mut depth = 0;
let mut quote = None;
let mut escape = false;
let mut index = open;
while index < source.len() {
let ch = source[index..].chars().next()?;
if escape {
escape = false;
index += ch.len_utf8();
continue;
}
if quote.is_some() {
if ch == '\\' {
escape = true;
} else if Some(ch) == quote {
quote = None;
}
index += ch.len_utf8();
continue;
}
if ch == '\'' || ch == '"' {
quote = Some(ch);
} else if ch == '[' {
depth += 1;
} else if ch == ']' {
depth -= 1;
if depth == 0 {
return Some(index);
}
}
index += ch.len_utf8();
}
None
}
fn split_top_level(source: &str) -> Vec<String> { fn split_top_level(source: &str) -> Vec<String> {
if source.trim().is_empty() { if source.trim().is_empty() {
return Vec::new(); return Vec::new();
@@ -1376,6 +1428,36 @@ mod tests {
assert!(parse("db.items.find({}).explain('executionStats').limit(1)").unwrap_err().contains("final")); assert!(parse("db.items.find({}).explain('executionStats').limit(1)").unwrap_err().contains("final"));
} }
#[test]
fn parses_bracket_collection_accessor() {
assert_eq!(
parse(r#"db["orders-2024"].find({a: 1})"#).unwrap(),
MongoCommand::Find {
collection: "orders-2024".to_string(),
filter: r#"{"a":1}"#.to_string(),
projection: None,
sort: None,
collation: None,
skip: 0,
limit: 100,
}
);
// Single quotes, a dotted name, and a chained method all behave like db.<name>.
assert_eq!(
parse("db['audit.logs'].count()").unwrap(),
MongoCommand::Count { collection: "audit.logs".to_string(), filter: "{}".to_string(), accurate: false }
);
assert!(matches!(
parse(r#"db["orders-2024"].updateOne({a: 1}, {$set: {b: 2}}, {upsert: true})"#).unwrap(),
MongoCommand::Update { many: false, .. }
));
for source in [r#"db[].find({})"#, r#"db[""].find({})"#, r#"db["x"]find({})"#, r#"db["x"]"#] {
assert!(parse(source).is_err(), "{source}");
}
}
#[test] #[test]
fn parses_get_collection_and_count() { fn parses_get_collection_and_count() {
assert_eq!( assert_eq!(
+23
View File
@@ -357,6 +357,29 @@ test("parseMongoFindCommand does not rewrite constructor text inside strings", (
assert.deepEqual(JSON.parse(command.filter), { label: "new Date()", note: "ObjectId()" }); assert.deepEqual(JSON.parse(command.filter), { label: "new Date()", note: "ObjectId()" });
}); });
test('parseMongoCommand accepts db["name"] bracket collection accessors', () => {
const find = parseMongoFindCommand('db["orders-2024"].find({a: 1})');
assert.ok(find);
assert.equal(find.collection, "orders-2024");
assert.deepEqual(JSON.parse(find.filter), { a: 1 });
// Single quotes and a dotted name behave like db.<name>.
assert.equal(parseMongoFindCommand("db['audit.logs'].find({})")?.collection, "audit.logs");
assert.deepEqual(parseMongoWriteCommand('db["orders-2024"].updateOne({a: 1}, {$set: {b: 2}}, {upsert: true})'), {
kind: "update",
collection: "orders-2024",
filter: '{"a": 1}',
update: '{"$set": {"b": 2}}',
options: '{"upsert": true}',
many: false,
});
for (const source of ["db[].find({})", 'db["x"]find({})', 'db["x"]']) {
assert.equal(parseMongoCommand(source), null, source);
}
});
test("parseMongoFindCommand accepts single-quoted string values and unquoted sort keys", () => { test("parseMongoFindCommand accepts single-quoted string values and unquoted sort keys", () => {
const command = parseMongoFindCommand("db.products.find({category: 'Electronics'}).sort({price: -1}).limit(2)"); const command = parseMongoFindCommand("db.products.find({category: 'Electronics'}).sort({price: -1}).limit(2)");
assert.ok(command); assert.ok(command);
+5
View File
@@ -202,6 +202,11 @@ export function parseCollectionMethodTarget(source: string, method: string): { c
if (getCollection) { if (getCollection) {
return { collection: getCollection[2]!, methodCallIndex: findChainedMethodCallIndex(source, method) }; return { collection: getCollection[2]!, methodCallIndex: findChainedMethodCallIndex(source, method) };
} }
// db["orders-2024"] reaches names that are not valid identifiers, the same way the shell does.
const bracket = new RegExp(`^db\\s*\\[\\s*(["'])(.*?)\\1\\s*\\]\\s*\\.\\s*${escapedMethod}\\s*\\(`).exec(source);
if (bracket) {
return { collection: bracket[2]!, methodCallIndex: findChainedMethodCallIndex(source, method) };
}
return null; return null;
} }