feat(mongo): preserve BSON types when cloning and saving documents
This commit is contained in:
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();
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in new issue
Block a user