fix(mongodb): preserve JSON-looking string values
This commit is contained in:
4 files changed
+103
-7
No files matched your search
@@ -305,7 +305,7 @@ async function gridSave(changes: DocumentGridChanges) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDoc = buildMongoUpdateDocument(dirtyCols, cols);
|
const updateDoc = buildMongoUpdateDocument(dirtyCols, cols, documents.value[rowIdx]);
|
||||||
if (Object.keys(updateDoc).length === 0) continue;
|
if (Object.keys(updateDoc).length === 0) continue;
|
||||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, String(id), JSON.stringify(updateDoc));
|
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);
|
const routing = documentRoutingFromGridRow(row, columns);
|
||||||
stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${JSON.stringify({ doc: updateDoc.$set ?? updateDoc }, null, 2)}`);
|
stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${JSON.stringify({ doc: updateDoc.$set ?? updateDoc }, null, 2)}`);
|
||||||
} else {
|
} else {
|
||||||
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns);
|
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, documents.value[rowIdx]);
|
||||||
stmts.push(`db.${coll}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
|
stmts.push(`db.${coll}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ const mongoQueryResultSaveHandler = computed<CustomSaveHandler | undefined>(() =
|
|||||||
const row = changes.rows[rowIdx];
|
const row = changes.rows[rowIdx];
|
||||||
const id = row?.[idColIdx];
|
const id = row?.[idColIdx];
|
||||||
if (id === null || id === undefined || String(id).trim() === "") continue;
|
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;
|
if (Object.keys(updateDoc).length === 0) continue;
|
||||||
await api.mongoUpdateDocument(tab.connectionId, tab.database, target.collection, String(id), JSON.stringify(updateDoc));
|
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 row = changes.rows[rowIdx];
|
||||||
const id = row?.[idColIdx];
|
const id = row?.[idColIdx];
|
||||||
if (id === null || id === undefined || String(id).trim() === "") continue;
|
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;
|
if (Object.keys(updateDoc).length === 0) continue;
|
||||||
stmts.push(`${mongoCollectionExpression(target.collection)}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
|
stmts.push(`${mongoCollectionExpression(target.collection)}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ export function parseMongoDocumentInputValue(raw: MongoInputValue): unknown {
|
|||||||
return raw;
|
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 {
|
function legacyMongoDateDisplayToExtendedJson(value: string): { $date: string } | null {
|
||||||
const match = value.match(LEGACY_MONGO_DATE_DISPLAY_PATTERN);
|
const match = value.match(LEGACY_MONGO_DATE_DISPLAY_PATTERN);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
@@ -38,7 +52,7 @@ function legacyMongoDateDisplayToExtendedJson(value: string): { $date: string }
|
|||||||
return { $date: `${date}T${time}.${millis.padEnd(3, "0")}Z` };
|
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 setFields: Record<string, unknown> = {};
|
||||||
const unsetFields: Record<string, unknown> = {};
|
const unsetFields: Record<string, unknown> = {};
|
||||||
for (const [colIdx, newVal] of changes) {
|
for (const [colIdx, newVal] of changes) {
|
||||||
@@ -47,7 +61,7 @@ export function buildMongoUpdateDocument(changes: Map<number, MongoInputValue>,
|
|||||||
if (newVal === null) {
|
if (newVal === null) {
|
||||||
unsetFields[col] = "";
|
unsetFields[col] = "";
|
||||||
} else {
|
} else {
|
||||||
setFields[col] = parseMongoDocumentInputValue(newVal);
|
setFields[col] = parseMongoExistingFieldInputValue(newVal, mongoDocumentFieldValue(originalDocument, col));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const doc: Record<string, unknown> = {};
|
const doc: Record<string, unknown> = {};
|
||||||
@@ -66,7 +80,7 @@ export function applyMongoGridChangesToDocument(document: unknown, changes: Map<
|
|||||||
if (newVal === null) {
|
if (newVal === null) {
|
||||||
delete updated[column];
|
delete updated[column];
|
||||||
} else {
|
} else {
|
||||||
updated[column] = parseMongoDocumentInputValue(newVal);
|
updated[column] = parseMongoExistingFieldInputValue(newVal, updated[column]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
|
|||||||
+82
@@ -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", () => {
|
test("applies saved Mongo grid changes to the raw preview document", () => {
|
||||||
const original = {
|
const original = {
|
||||||
_id: "1",
|
_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", () => {
|
test("builds Mongo inserts with parsed date values", () => {
|
||||||
assert.deepEqual(buildMongoInsertDocument(["ignored", 'new Date("2026-06-10T13:59:31.287Z")'], ["_id", "createdAt"]), {
|
assert.deepEqual(buildMongoInsertDocument(["ignored", 'new Date("2026-06-10T13:59:31.287Z")'], ["_id", "createdAt"]), {
|
||||||
createdAt: { $date: "2026-06-10T13:59:31.287Z" },
|
createdAt: { $date: "2026-06-10T13:59:31.287Z" },
|
||||||
|
|||||||
Reference in new issue
Block a user