fix(mongodb): preserve JSON-looking string values

This commit is contained in:
zipg 2026-07-13 18:33:30 +08:00 committed by GitHub
parent 66d7b965dc
commit 5ceca7fb3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 7 deletions

View File

@ -305,7 +305,7 @@ async function gridSave(changes: DocumentGridChanges) {
continue;
}
const updateDoc = buildMongoUpdateDocument(dirtyCols, cols);
const updateDoc = buildMongoUpdateDocument(dirtyCols, cols, documents.value[rowIdx]);
if (Object.keys(updateDoc).length === 0) continue;
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, String(id), JSON.stringify(updateDoc));
}
@ -382,7 +382,7 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
const routing = documentRoutingFromGridRow(row, columns);
stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${JSON.stringify({ doc: updateDoc.$set ?? updateDoc }, null, 2)}`);
} else {
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns);
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, documents.value[rowIdx]);
stmts.push(`db.${coll}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
}
}

View File

@ -391,7 +391,7 @@ const mongoQueryResultSaveHandler = computed<CustomSaveHandler | undefined>(() =
const row = changes.rows[rowIdx];
const id = row?.[idColIdx];
if (id === null || id === undefined || String(id).trim() === "") continue;
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns);
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns, tab.result?.mongo_documents?.[rowIdx]);
if (Object.keys(updateDoc).length === 0) continue;
await api.mongoUpdateDocument(tab.connectionId, tab.database, target.collection, String(id), JSON.stringify(updateDoc));
}
@ -405,7 +405,7 @@ const mongoQueryResultSaveHandler = computed<CustomSaveHandler | undefined>(() =
const row = changes.rows[rowIdx];
const id = row?.[idColIdx];
if (id === null || id === undefined || String(id).trim() === "") continue;
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns);
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns, tab.result?.mongo_documents?.[rowIdx]);
if (Object.keys(updateDoc).length === 0) continue;
stmts.push(`${mongoCollectionExpression(target.collection)}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
}

View File

@ -31,6 +31,20 @@ export function parseMongoDocumentInputValue(raw: MongoInputValue): unknown {
return raw;
}
function parseMongoExistingFieldInputValue(raw: Exclude<MongoInputValue, null>, originalValue: unknown): unknown {
// Objects and arrays are serialized into grid text too, so the raw document
// is the only reliable way to distinguish them from JSON-shaped BSON strings.
if (typeof originalValue === "string") {
return typeof raw === "string" ? raw : String(raw);
}
return parseMongoDocumentInputValue(raw);
}
function mongoDocumentFieldValue(document: unknown, field: string): unknown {
if (!document || typeof document !== "object" || Array.isArray(document)) return undefined;
return (document as Record<string, unknown>)[field];
}
function legacyMongoDateDisplayToExtendedJson(value: string): { $date: string } | null {
const match = value.match(LEGACY_MONGO_DATE_DISPLAY_PATTERN);
if (!match) return null;
@ -38,7 +52,7 @@ function legacyMongoDateDisplayToExtendedJson(value: string): { $date: string }
return { $date: `${date}T${time}.${millis.padEnd(3, "0")}Z` };
}
export function buildMongoUpdateDocument(changes: Map<number, MongoInputValue>, columns: string[]): Record<string, unknown> {
export function buildMongoUpdateDocument(changes: Map<number, MongoInputValue>, columns: string[], originalDocument?: unknown): Record<string, unknown> {
const setFields: Record<string, unknown> = {};
const unsetFields: Record<string, unknown> = {};
for (const [colIdx, newVal] of changes) {
@ -47,7 +61,7 @@ export function buildMongoUpdateDocument(changes: Map<number, MongoInputValue>,
if (newVal === null) {
unsetFields[col] = "";
} else {
setFields[col] = parseMongoDocumentInputValue(newVal);
setFields[col] = parseMongoExistingFieldInputValue(newVal, mongoDocumentFieldValue(originalDocument, col));
}
}
const doc: Record<string, unknown> = {};
@ -66,7 +80,7 @@ export function applyMongoGridChangesToDocument(document: unknown, changes: Map<
if (newVal === null) {
delete updated[column];
} else {
updated[column] = parseMongoDocumentInputValue(newVal);
updated[column] = parseMongoExistingFieldInputValue(newVal, updated[column]);
}
}
return updated;

View File

@ -36,6 +36,70 @@ test("builds Mongo grid updates with set and unset operators", () => {
});
});
test("preserves JSON-shaped strings when updating existing Mongo fields", () => {
const original = {
_id: "1",
answer: '{"action":"New","values":[1]}',
tagsText: '["draft"]',
profile: { role: "admin" },
};
const changes = new Map<number, string | number | boolean | null>([
[1, '{\n "action": "Updated",\n "values": [\n 1,\n 2\n ]\n}'],
[2, '[\n "published"\n]'],
[3, '{"role":"maintainer"}'],
]);
const update = buildMongoUpdateDocument(changes, ["_id", "answer", "tagsText", "profile"], original);
assert.deepEqual(update, {
$set: {
answer: '{\n "action": "Updated",\n "values": [\n 1,\n 2\n ]\n}',
tagsText: '[\n "published"\n]',
profile: { role: "maintainer" },
},
});
assert.equal(formatMongoShellLiteral(update), '{"$set":{"answer":"{\\n \\"action\\": \\"Updated\\",\\n \\"values\\": [\\n 1,\\n 2\\n ]\\n}","tagsText":"[\\n \\"published\\"\\n]","profile":{"role":"maintainer"}}}');
});
test("preserves existing Mongo strings that resemble typed literals", () => {
const original = {
_id: "1",
numericText: "42",
booleanText: "true",
dateText: 'ISODate("2026-01-01T00:00:00.000Z")',
quotedText: '"literal"',
};
const changes = new Map<number, string | number | boolean | null>([
[1, "43"],
[2, "false"],
[3, 'ISODate("2026-02-01T00:00:00.000Z")'],
[4, '"changed"'],
]);
assert.deepEqual(buildMongoUpdateDocument(changes, ["_id", "numericText", "booleanText", "dateText", "quotedText"], original), {
$set: {
numericText: "43",
booleanText: "false",
dateText: 'ISODate("2026-02-01T00:00:00.000Z")',
quotedText: '"changed"',
},
});
});
test("keeps JSON inference for fields without an existing Mongo type", () => {
const changes = new Map<number, string | number | boolean | null>([
[1, '{"enabled":true}'],
[2, "42"],
]);
assert.deepEqual(buildMongoUpdateDocument(changes, ["_id", "newObject", "newNumber"], { _id: "1" }), {
$set: {
newObject: { enabled: true },
newNumber: 42,
},
});
});
test("applies saved Mongo grid changes to the raw preview document", () => {
const original = {
_id: "1",
@ -62,6 +126,24 @@ test("applies saved Mongo grid changes to the raw preview document", () => {
});
});
test("applies Mongo grid edits without converting existing JSON strings", () => {
const original = {
_id: "1",
answer: '{"action":"New"}',
profile: { role: "admin" },
};
const changes = new Map<number, string | number | boolean | null>([
[1, '{\n "action": "Updated"\n}'],
[2, '{"role":"maintainer"}'],
]);
assert.deepEqual(applyMongoGridChangesToDocument(original, changes, ["_id", "answer", "profile"]), {
_id: "1",
answer: '{\n "action": "Updated"\n}',
profile: { role: "maintainer" },
});
});
test("builds Mongo inserts with parsed date values", () => {
assert.deepEqual(buildMongoInsertDocument(["ignored", 'new Date("2026-06-10T13:59:31.287Z")'], ["_id", "createdAt"]), {
createdAt: { $date: "2026-06-10T13:59:31.287Z" },