feat(mongo): preserve BSON types when cloning and saving documents

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-10 23:02:37 +08:00
1 parent cc7112b240
commit a5ce8510e7
12 files changed
+239 -36

No files matched your search

@@ -68,7 +68,19 @@ import {
import { applyDocumentStoreIdentityPlan, insertDocumentStoreDocument as insertDocumentStoreDocumentCore } from "@/lib/app/documentStoreSave";
import RedisJsonEditor from "@/components/redis/RedisJsonEditor.vue";
import { isLosslessJsonNumber, parseJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat";
import { buildMongoInsertDocument, buildMongoUpdateDocument, formatMongoShellLiteral, mongoDocumentDisplayValue, mongoDocumentGridColumnTypes, mongoDocumentIdForGrid, parseMongoDocumentInputValue, serializeMongoDocumentId, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues";
import {
buildMongoCopyDocumentFromOriginal,
buildMongoInsertDocument,
buildMongoUpdateDocument,
formatMongoShellLiteral,
mongoDocumentDisplayValue,
mongoDocumentGridColumnTypes,
mongoDocumentIdForGrid,
parseMongoDocumentInputValue,
serializeMongoDocumentId,
type MongoInputValue,
} from "@/lib/mongo/mongoDocumentValues";
import type { GridNewRowMeta } from "@/lib/dataGrid/gridNewRowPlacement";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { findDocumentTextMatches, renderDocumentJsonHtml } from "@/lib/document/documentJsonSearch";
import { documentDataGridColumnLayoutScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
@@ -246,6 +258,7 @@ type DocumentGridChanges = {
dirtyRows: Map<number, Map<number, MongoInputValue>>;
deletedRows: Set<number>;
newRows: MongoInputValue[][];
newRowMeta: GridNewRowMeta[];
columns: string[];
rows: MongoInputValue[][];
};
@@ -756,8 +769,9 @@ async function gridSave(changes: DocumentGridChanges) {
await api.documentDeleteDocument(props.connectionId, props.database, props.collection, isEs ? String(documentId) : serializeMongoDocumentId(documentId), routing, documentType);
}
for (const newRow of changes.newRows) {
const doc = isEs ? buildElasticsearchInsertDocument(newRow, cols) : buildMongoInsertDocument(newRow, cols);
for (const [newRowIndex, newRow] of changes.newRows.entries()) {
const newRowMeta = changes.newRowMeta[newRowIndex];
const doc = isEs ? buildElasticsearchInsertDocument(newRow, cols) : buildMongoGridInsertDocument(newRow, cols, newRowMeta);
if (isEs) {
const id = documentIdFromGridValue(newRow[idColIdx]);
const routing = documentRoutingFromGridRow(newRow, cols);
@@ -768,7 +782,9 @@ async function gridSave(changes: DocumentGridChanges) {
}
continue;
}
await api.documentInsertDocument(props.connectionId, props.database, props.collection, JSON.stringify(doc));
const sourceIndex = newRowMeta?.sourceIndex;
const preserveBsonTypes = sourceIndex !== undefined && copyDocuments.value[sourceIndex] !== undefined;
await api.documentInsertDocument(props.connectionId, props.database, props.collection, JSON.stringify(doc), undefined, preserveBsonTypes);
}
if (isEs) resetElasticsearchTotals({ preservePaginationTotal: true });
@@ -786,6 +802,22 @@ function buildElasticsearchInsertDocument(row: MongoInputValue[], columns: strin
return doc;
}
function buildMongoGridInsertDocument(row: MongoInputValue[], columns: string[], meta?: GridNewRowMeta): Record<string, unknown> {
const sourceIndex = meta?.sourceIndex;
const sourceDocument = sourceIndex === undefined ? undefined : copyDocuments.value[sourceIndex];
if (!sourceDocument) return buildMongoInsertDocument(row, columns);
const editedColumns = new Set(meta?.editedColumns);
return (
buildMongoCopyDocumentFromOriginal(
sourceDocument,
row,
columns,
columns.map((_, index) => editedColumns.has(index)),
{ excludePrimaryKeys: true },
) ?? buildMongoInsertDocument(row, columns)
);
}
function elasticsearchPathIdPreview(id: string): string {
return encodeURIComponent(id);
}
@@ -805,7 +837,7 @@ function buildElasticsearchPartialUpdateDocument(changes: Map<number, MongoInput
}
async function previewDocumentChanges(changes: DocumentGridChanges): Promise<string[]> {
const { dirtyRows, deletedRows, newRows, columns, rows } = changes;
const { dirtyRows, deletedRows, newRows, newRowMeta, columns, rows } = changes;
const idColIdx = columns.indexOf("_id");
const stmts: string[] = [];
const coll = props.collection;
@@ -837,8 +869,8 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
}
}
for (const newRow of newRows) {
const doc = isEs ? buildElasticsearchInsertDocument(newRow, columns) : buildMongoInsertDocument(newRow, columns);
for (const [newRowIndex, newRow] of newRows.entries()) {
const doc = isEs ? buildElasticsearchInsertDocument(newRow, columns) : buildMongoGridInsertDocument(newRow, columns, newRowMeta[newRowIndex]);
if (isEs) {
const id = idColIdx >= 0 ? documentIdFromGridValue(newRow[idColIdx]) : null;
if (id) {
@@ -60,9 +60,9 @@ type GridScrollerRef =
};
export interface CustomSaveHandler {
save: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<void>;
save: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; newRowMeta: GridNewRowMeta[]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<void>;
applySavedChanges?: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; columns: string[] }) => void;
preview?: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<string[]>;
preview?: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; newRowMeta: GridNewRowMeta[]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<string[]>;
canInsert?: boolean;
canDelete?: boolean;
readonlyColumns?: string[];
@@ -126,6 +126,7 @@ interface PendingChangesSnapshot {
interface PendingSaveSnapshot {
newRows: CellValue[][];
newRowRefs: CellValue[][];
newRowMeta: GridNewRowMeta[];
dirtyRows: Map<number, Map<number, CellValue>>;
deletedRows: Set<number>;
}
@@ -220,11 +221,37 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
// Kept in lockstep with every structural mutation of newRows.
const newRowMeta = ref<GridNewRowMeta[]>([]);
let nextNewRowToken = 1;
function allocateNewRowMeta(placement: GridNewRowPlacement | null): GridNewRowMeta {
return { token: nextNewRowToken++, placement };
function allocateNewRowMeta(placement: GridNewRowPlacement | null, sourceIndex?: number, editedColumns?: readonly number[]): GridNewRowMeta {
return { token: nextNewRowToken++, placement, sourceIndex, editedColumns: editedColumns?.length ? [...editedColumns] : undefined };
}
function cloneNewRowMeta(meta: readonly GridNewRowMeta[]): GridNewRowMeta[] {
return meta.map((item) => ({ token: item.token, placement: item.placement ? { ...item.placement } : null }));
return meta.map((item) => ({ token: item.token, placement: item.placement ? { ...item.placement } : null, sourceIndex: item.sourceIndex, editedColumns: item.editedColumns ? [...item.editedColumns] : undefined }));
}
function updateClonedRowEditedColumns(newIndex: number, col: number, value: CellValue) {
const meta = newRowMeta.value[newIndex];
if (!meta || meta.sourceIndex === undefined) return;
const baseline = result.value.rows[meta.sourceIndex]?.[col];
const edited = new Set(meta.editedColumns);
if (value === baseline) edited.delete(col);
else edited.add(col);
meta.editedColumns = edited.size > 0 ? [...edited].sort((left, right) => left - right) : undefined;
}
function clonedRowMeta(item: RowItem, row: readonly CellValue[]): GridNewRowMeta {
const inherited = item.newIndex === undefined ? undefined : newRowMeta.value[item.newIndex];
const sourceIndex = item.sourceIndex ?? inherited?.sourceIndex;
const edited = new Set(inherited?.editedColumns);
if (item.sourceIndex !== undefined) {
for (const column of dirtyRows.value.get(item.sourceIndex)?.keys() ?? []) edited.add(column);
}
if (sourceIndex !== undefined) {
const baseline = result.value.rows[sourceIndex] ?? [];
row.forEach((value, column) => {
if (value !== baseline[column]) edited.add(column);
});
}
return allocateNewRowMeta(null, sourceIndex, [...edited]);
}
// Restore a metadata snapshot and resume token allocation past its maximum so
// newly created rows never collide with tokens held by restored rows (a fresh
@@ -742,8 +769,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
if (changed) pushUndoSnapshot();
if (newRows.value[item.newIndex]) {
newRows.value[item.newIndex][col] = newVal;
updateClonedRowEditedColumns(item.newIndex, col, newVal);
}
newRows.value = [...newRows.value];
newRowMeta.value = [...newRowMeta.value];
if (changed) touchPendingChanges();
editingCell.value = null;
isCommitting = false;
@@ -849,9 +878,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
pushUndoSnapshot();
}
row[col] = newVal;
updateClonedRowEditedColumns(item.newIndex, col, newVal);
markBatchMutated();
if (!isBatching) {
newRows.value = [...newRows.value];
newRowMeta.value = [...newRowMeta.value];
touchPendingChanges();
}
return;
@@ -924,7 +955,9 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
if (!row || row[col] === null) return;
pushUndoSnapshot();
row[col] = null;
updateClonedRowEditedColumns(item.newIndex, col, null);
newRows.value = [...newRows.value];
newRowMeta.value = [...newRowMeta.value];
touchPendingChanges();
return;
}
@@ -1111,7 +1144,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
pushUndoSnapshot();
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
newRows.value.push(clonedData);
newRowMeta.value.push(allocateNewRowMeta(null));
newRowMeta.value.push(clonedRowMeta(item, clonedData));
newRows.value = [...newRows.value];
newRowMeta.value = [...newRowMeta.value];
touchPendingChanges();
@@ -1134,7 +1167,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
for (const item of rowsToClone) {
const clonedData = clonedRowData(item);
newRows.value.push(clonedData);
newRowMeta.value.push(allocateNewRowMeta(null));
newRowMeta.value.push(clonedRowMeta(item, clonedData));
}
newRows.value = [...newRows.value];
newRowMeta.value = [...newRowMeta.value];
@@ -1263,6 +1296,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
dirtyRows: new Map([...dirtyRows.value.entries()].map(([rowIndex, changes]) => [rowIndex, new Map(changes)])),
newRows: currentNewRows.map((row) => [...row]),
newRowRefs: currentNewRows,
newRowMeta: cloneNewRowMeta(newRowMeta.value),
deletedRows: new Set(deletedRows.value),
};
}
@@ -1452,6 +1486,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
await customHandler.save({
dirtyRows: snapshot.dirtyRows,
newRows: snapshot.newRows,
newRowMeta: snapshot.newRowMeta,
deletedRows: snapshot.deletedRows,
columns: result.value.columns,
rows: result.value.rows,
@@ -1680,7 +1715,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
try {
if (customSaveHandler?.value) {
const preview = customSaveHandler.value.preview;
if (preview) return await preview({ dirtyRows: dirtyRows.value, newRows: newRows.value, deletedRows: deletedRows.value, columns: result.value.columns, rows: result.value.rows });
if (preview) return await preview({ dirtyRows: dirtyRows.value, newRows: newRows.value, newRowMeta: cloneNewRowMeta(newRowMeta.value), deletedRows: deletedRows.value, columns: result.value.columns, rows: result.value.rows });
return [];
}
const stmtOptions = saveStatementOptions();
+2 -1
View File
@@ -3584,13 +3584,14 @@ export async function mongoInsertDocument(connectionId: string, database: string
return documentInsertDocument(connectionId, database, collection, docJson, routing);
}
export async function documentInsertDocument(connectionId: string, database: string, collection: string, docJson: string, routing?: string): Promise<string> {
export async function documentInsertDocument(connectionId: string, database: string, collection: string, docJson: string, routing?: string, preserveBsonTypes?: boolean): Promise<string> {
return post("/api/document-store/insert-document", {
connectionId,
database,
collection,
docJson,
routing,
preserveBsonTypes,
});
}
+2 -1
View File
@@ -3523,13 +3523,14 @@ export async function mongoInsertDocument(connectionId: string, database: string
return documentInsertDocument(connectionId, database, collection, docJson, routing);
}
export async function documentInsertDocument(connectionId: string, database: string, collection: string, docJson: string, routing?: string): Promise<string> {
export async function documentInsertDocument(connectionId: string, database: string, collection: string, docJson: string, routing?: string, preserveBsonTypes?: boolean): Promise<string> {
return invoke("document_insert_document", {
connectionId,
database,
collection,
docJson,
routing,
preserveBsonTypes,
});
}
@@ -25,6 +25,10 @@ export interface GridNewRowMeta {
token: number;
/** Display placement; `null` appends at the end of the result. */
placement: GridNewRowPlacement | null;
/** Source result row for a cloned pending row, when it has one. */
sourceIndex?: number;
/** Columns whose values differ from the cloned source row. */
editedColumns?: number[];
}
export type GridOrderedRowEntry = { kind: "source"; sourceIndex: number } | { kind: "new"; newIndex: number };