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:
parent
a212ccbfa7
commit
78019c44e2
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue