fix(mongo): support ISODate() and new Date() in MongoDB queries

* fix(mongo): hỗ trợ ISODate() và new Date() trong query MongoDB

Parser shell MongoDB ở frontend chỉ chuyển ObjectId(...) sang extended JSON,
nên filter chứa ISODate("...")/new Date("...") không parse được: câu lệnh rơi
xuống SQL executor và báo lỗi "Use MongoDB-specific commands".

Bổ sung rewrite ISODate(x)/new Date(x) -> {"$date":x} trong normalizeJsonArgument,
khớp với json_value_to_bson của backend (đã hiểu $date/$oid). Thêm test cho
filter dùng ISODate và new Date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mongo): decode $date extended JSON trong filter query

json_filter_value_to_bson chỉ chuyển {"$oid":...} nên filter chứa {"$date":...}
bị gửi thẳng lên server: { field: {"$date":...} } lỗi "unknown operator: $date",
còn { field: {"$gte": {"$date":...}} } so sánh với sub-document nên không khớp gì
(trả về rỗng).

Thêm nhánh parse_extended_json_date vào json_filter_value_to_bson, đồng bộ với
json_value_to_bson (vốn đã hiểu $date/$oid). Thêm test cho cả equality và $gte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hiep Le 2026-07-06 17:21:15 +07:00 committed by GitHub
parent a212ccbfa7
commit 78019c44e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 1 deletions

View File

@ -618,7 +618,14 @@ function parseCollectionMethodTarget(source: string, method: string): { collecti
function normalizeJsonArgument(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return "{}";
const preprocessed = quoteUnquotedObjectKeys(convertSingleQuotedStrings(trimmed.replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}')));
// Rewrite mongo shell constructors that are not valid JSON into the extended
// JSON the backend understands (mongo_driver::json_value_to_bson): ObjectId(x)
// -> {"$oid":x} and ISODate(x)/new Date(x) -> {"$date":x}. Without this a
// filter such as { createdAt: { $gte: ISODate("...") } } fails JSON.parse,
// the command is left unrecognized and falls through to the SQL executor,
// which rejects it with "Use MongoDB-specific commands".
const withExtendedJson = trimmed.replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}').replace(/(?:ISODate|new\s+Date)\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$date":"$1"}');
const preprocessed = quoteUnquotedObjectKeys(convertSingleQuotedStrings(withExtendedJson));
try {
JSON.parse(preprocessed);
return preprocessed;

View File

@ -1167,6 +1167,13 @@ fn json_filter_value_to_bson(value: &serde_json::Value, field_name: Option<&str>
return Bson::ObjectId(oid);
}
}
// Extended JSON dates must be decoded in filters too, otherwise
// {"$date": ...} reaches the server as a raw document: a bare
// { field: {"$date": ...} } fails with "unknown operator: $date"
// and { field: {"$gte": {"$date": ...}} } silently matches nothing.
if let Some(date) = parse_extended_json_date(obj) {
return Bson::DateTime(date);
}
}
if field_name == Some("_id") && obj.keys().all(|key| key.starts_with('$')) {
@ -1350,6 +1357,27 @@ mod tests {
assert!(matches!(doc.get("owner_id"), Some(Bson::String(value)) if value == id));
}
#[test]
fn json_filter_to_document_decodes_extended_json_dates() {
let iso = "2025-02-25T04:57:39.965Z";
let expected = DateTime::parse_rfc3339_str(iso).unwrap();
// Direct equality must yield a BSON DateTime, not a raw { "$date": ... }
// document that the server rejects with "unknown operator: $date".
let filter = serde_json::json!({ "createdAt": { "$date": iso } });
let doc = json_filter_to_document(&filter).unwrap();
assert_eq!(doc.get("createdAt"), Some(&Bson::DateTime(expected)));
// Range operands must be decoded too, otherwise $gte compares against a
// sub-document and silently matches nothing.
let range = serde_json::json!({ "createdAt": { "$gte": { "$date": iso } } });
let range_doc = json_filter_to_document(&range).unwrap();
let Some(Bson::Document(op)) = range_doc.get("createdAt") else {
panic!("expected operator document");
};
assert_eq!(op.get("$gte"), Some(&Bson::DateTime(expected)));
}
#[test]
fn bson_to_json_displays_date_as_mongo_isodate() {
let date = DateTime::parse_rfc3339_str("2026-06-10T13:59:31.287Z").unwrap();

View File

@ -65,6 +65,23 @@ test("parseMongoFindCommand accepts Compass-style unquoted keys and ObjectId", (
assert.deepEqual(JSON.parse(command.filter), { _id: { $oid: "6a045a92d2971e44243771a1" } });
});
test("parseMongoFindCommand rewrites ISODate into extended JSON $date", () => {
const command = parseMongoFindCommand(`db.trainingdocuments.find({
createdAt: { $gte: ISODate("2025-02-25T04:57:39.965Z") }
})`);
assert.ok(command);
assert.equal(command.collection, "trainingdocuments");
assert.deepEqual(JSON.parse(command.filter), { createdAt: { $gte: { $date: "2025-02-25T04:57:39.965Z" } } });
});
test("parseMongoFindCommand rewrites new Date and single-quoted ISODate", () => {
const command = parseMongoFindCommand("db.events.find({ at: { $lt: new Date('2025-01-01T00:00:00Z'), $gte: ISODate('2024-01-01T00:00:00Z') } })");
assert.ok(command);
assert.deepEqual(JSON.parse(command.filter), {
at: { $lt: { $date: "2025-01-01T00:00:00Z" }, $gte: { $date: "2024-01-01T00:00:00Z" } },
});
});
test("parseMongoFindCommand accepts single-quoted string values and unquoted sort keys", () => {
const command = parseMongoFindCommand("db.products.find({category: 'Electronics'}).sort({price: -1}).limit(2)");
assert.ok(command);