fix(mongo): preserve document IDs during editing

This commit is contained in:
vrustx authored and GitHub committed 2026-07-16 00:47:02 +08:00
1 parent 295b67dd36
commit d9076f4689
6 files changed
+44 -30

No files matched your search

@@ -367,12 +367,6 @@ function buildElasticsearchInsertDocument(row: MongoInputValue[], columns: strin
return doc; return doc;
} }
function mongoIdPreview(val: unknown): string {
if (val === null || val === undefined) return "null";
if (typeof val === "string" && /^[a-fA-F0-9]{24}$/.test(val)) return `ObjectId("${val}")`;
return formatMongoShellLiteral(val);
}
function elasticsearchPathIdPreview(id: string): string { function elasticsearchPathIdPreview(id: string): string {
return encodeURIComponent(id); return encodeURIComponent(id);
} }
@@ -408,7 +402,7 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${stringifyDocumentStoreValue({ doc: updateDoc.$set ?? updateDoc }, "elasticsearch", 2)}`); stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${stringifyDocumentStoreValue({ doc: updateDoc.$set ?? updateDoc }, "elasticsearch", 2)}`);
} else { } else {
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, documents.value[rowIdx]); const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, documents.value[rowIdx]);
stmts.push(`db.${coll}.updateOne({_id: ${mongoIdPreview(documents.value[rowIdx]?._id ?? id)}}, ${formatMongoShellLiteral(updateDoc)})`); stmts.push(`db.${coll}.updateOne({_id: ${formatMongoShellLiteral(documents.value[rowIdx]?._id ?? id)}}, ${formatMongoShellLiteral(updateDoc)})`);
} }
} }
@@ -420,7 +414,7 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
const routing = documentRoutingFromGridRow(row, columns); const routing = documentRoutingFromGridRow(row, columns);
stmts.push(`DELETE /${coll}/_doc/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}`); stmts.push(`DELETE /${coll}/_doc/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}`);
} else { } else {
stmts.push(`db.${coll}.deleteOne({_id: ${mongoIdPreview(documents.value[rowIdx]?._id ?? id)}})`); stmts.push(`db.${coll}.deleteOne({_id: ${formatMongoShellLiteral(documents.value[rowIdx]?._id ?? id)}})`);
} }
} }
@@ -382,11 +382,6 @@ type MongoQueryGridChanges = {
columns: string[]; columns: string[];
rows: MongoInputValue[][]; rows: MongoInputValue[][];
}; };
function mongoIdPreview(val: unknown): string {
if (val === null || val === undefined) return "null";
if (typeof val === "string" && /^[a-fA-F0-9]{24}$/.test(val)) return `ObjectId("${val}")`;
return formatMongoShellLiteral(val);
}
function mongoCollectionExpression(collection: string): string { function mongoCollectionExpression(collection: string): string {
return `db.getCollection(${JSON.stringify(collection)})`; return `db.getCollection(${JSON.stringify(collection)})`;
} }
@@ -427,7 +422,7 @@ const mongoQueryResultSaveHandler = computed<CustomSaveHandler | undefined>(() =
if (id === null || id === undefined || String(id).trim() === "") continue; if (id === null || id === undefined || String(id).trim() === "") continue;
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns, tab.result?.mongo_documents?.[rowIdx]); const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns, tab.result?.mongo_documents?.[rowIdx]);
if (Object.keys(updateDoc).length === 0) continue; if (Object.keys(updateDoc).length === 0) continue;
stmts.push(`${mongoCollectionExpression(target.collection)}.updateOne({_id: ${mongoIdPreview(mongoQueryResultDocumentId(rowIdx, id))}}, ${formatMongoShellLiteral(updateDoc)})`); stmts.push(`${mongoCollectionExpression(target.collection)}.updateOne({_id: ${formatMongoShellLiteral(mongoQueryResultDocumentId(rowIdx, id))}}, ${formatMongoShellLiteral(updateDoc)})`);
} }
return stmts; return stmts;
}; };
@@ -174,7 +174,7 @@ export function serializeMongoDocumentId(value: unknown): string {
} }
export function mongoDocumentIdForGrid(value: unknown): MongoInputValue { export function mongoDocumentIdForGrid(value: unknown): MongoInputValue {
if (isMongoNumberLong(value)) return value.$numberLong; if (isMongoExtendedJsonId(value)) return String(value.$numberLong ?? value.$oid);
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value; if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
return JSON.stringify(value); return JSON.stringify(value);
} }
@@ -185,7 +185,3 @@ function isMongoExtendedJsonId(value: unknown): value is Record<string, unknown>
const keys = Object.keys(object); const keys = Object.keys(object);
return keys.length === 1 && (typeof object.$numberLong === "string" || typeof object.$oid === "string"); return keys.length === 1 && (typeof object.$numberLong === "string" || typeof object.$oid === "string");
} }
function isMongoNumberLong(value: unknown): value is { $numberLong: string } {
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 1 && typeof (value as Record<string, unknown>).$numberLong === "string";
}
+14 -2
View File
@@ -1341,8 +1341,10 @@ fn bson_to_json(bson: &Bson) -> serde_json::Value {
fn bson_document_field_to_json(key: &str, bson: &Bson) -> serde_json::Value { fn bson_document_field_to_json(key: &str, bson: &Bson) -> serde_json::Value {
if key == "_id" { if key == "_id" {
if let Bson::Int64(value) = bson { match bson {
return serde_json::json!({ "$numberLong": value.to_string() }); Bson::Int64(value) => return serde_json::json!({ "$numberLong": value.to_string() }),
Bson::ObjectId(value) => return serde_json::json!({ "$oid": value.to_hex() }),
_ => {}
} }
} }
bson_to_json(bson) bson_to_json(bson)
@@ -1870,6 +1872,16 @@ mod tests {
assert_eq!(value["snowflake"], serde_json::json!("2048938405781032962")); assert_eq!(value["snowflake"], serde_json::json!("2048938405781032962"));
} }
#[test]
fn bson_to_json_preserves_object_id_type_for_updates() {
let oid = ObjectId::parse_str("507f1f77bcf86cd799439011").unwrap();
let value = bson_to_json(&Bson::Document(doc! {
"_id": Bson::ObjectId(oid),
}));
assert_eq!(value["_id"], serde_json::json!({ "$oid": "507f1f77bcf86cd799439011" }));
}
#[test] #[test]
fn bson_to_json_keeps_safe_int64_as_number() { fn bson_to_json_keeps_safe_int64_as_number() {
let value = bson_to_json(&Bson::Int64(42)); let value = bson_to_json(&Bson::Int64(42));
+9 -4
View File
@@ -222,13 +222,18 @@ test("formats extended JSON dates as Mongo shell ISODate literals", () => {
test("formats extended JSON object ids as Mongo shell ObjectId literals", () => { test("formats extended JSON object ids as Mongo shell ObjectId literals", () => {
assert.equal(formatMongoShellLiteral({ $oid: "6743e4bfa3f6f84bc3fff6c8" }), 'ObjectId("6743e4bfa3f6f84bc3fff6c8")'); assert.equal(formatMongoShellLiteral({ $oid: "6743e4bfa3f6f84bc3fff6c8" }), 'ObjectId("6743e4bfa3f6f84bc3fff6c8")');
assert.equal(formatMongoShellLiteral("6743e4bfa3f6f84bc3fff6c8"), '"6743e4bfa3f6f84bc3fff6c8"');
}); });
test("serializes typed Mongo document ids while keeping their grid display compact", () => { test("serializes typed Mongo document ids while keeping their grid display compact", () => {
const id = { $numberLong: "2048938405781032962" }; const longId = { $numberLong: "2048938405781032962" };
assert.equal(serializeMongoDocumentId(id), '{"$numberLong":"2048938405781032962"}'); const objectId = { $oid: "6743e4bfa3f6f84bc3fff6c8" };
assert.equal(mongoDocumentIdForGrid(id), "2048938405781032962"); assert.equal(serializeMongoDocumentId(longId), '{"$numberLong":"2048938405781032962"}');
assert.equal(serializeMongoDocumentId({ $oid: "6743e4bfa3f6f84bc3fff6c8" }), '{"$oid":"6743e4bfa3f6f84bc3fff6c8"}'); assert.equal(mongoDocumentIdForGrid(longId), "2048938405781032962");
assert.equal(serializeMongoDocumentId(objectId), '{"$oid":"6743e4bfa3f6f84bc3fff6c8"}');
assert.equal(mongoDocumentIdForGrid(objectId), "6743e4bfa3f6f84bc3fff6c8");
assert.equal(serializeMongoDocumentId(42), "42");
assert.equal(serializeMongoDocumentId(42.5), "42.5");
assert.equal(serializeMongoDocumentId("2048938405781032962"), '__dbx_mongo_string_id__"2048938405781032962"'); assert.equal(serializeMongoDocumentId("2048938405781032962"), '__dbx_mongo_string_id__"2048938405781032962"');
assert.equal(serializeMongoDocumentId('{"$numberLong":"2048938405781032962"}'), '__dbx_mongo_string_id__"{\\"$numberLong\\":\\"2048938405781032962\\"}"'); assert.equal(serializeMongoDocumentId('{"$numberLong":"2048938405781032962"}'), '__dbx_mongo_string_id__"{\\"$numberLong\\":\\"2048938405781032962\\"}"');
}); });
+17 -5
View File
@@ -626,12 +626,24 @@ test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => {
assert.equal(result.truncated, true); assert.equal(result.truncated, true);
}); });
test("mongoDocumentsToQueryResult displays typed int64 ids without losing raw type metadata", () => { test("mongoDocumentsToQueryResult displays ids without losing raw type metadata", () => {
const id = { $numberLong: "2048938405781032962" }; const documents = [
const result = mongoDocumentsToQueryResult([{ _id: id, name: "snowflake" }], 1, 1); { _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, name: "object id" },
{ _id: { $numberLong: "2048938405781032962" }, name: "int64" },
{ _id: 42, name: "int" },
{ _id: 42.5, name: "double" },
{ _id: "customer-42", name: "string" },
];
const result = mongoDocumentsToQueryResult(documents, documents.length, documents.length);
assert.deepEqual(result.rows, [["2048938405781032962", "snowflake"]]); assert.deepEqual(result.rows, [
assert.deepEqual(result.mongo_documents, [{ _id: id, name: "snowflake" }]); ["6743e4bfa3f6f84bc3fff6c8", "object id"],
["2048938405781032962", "int64"],
[42, "int"],
[42.5, "double"],
["customer-42", "string"],
]);
assert.deepEqual(result.mongo_documents, documents);
}); });
test("buildMongoUpdateDocument ignores _id and preserves typed values", () => { test("buildMongoUpdateDocument ignores _id and preserves typed values", () => {