feat(mongodb): support editing find query results

This commit is contained in:
zipg 2026-06-28 18:59:27 +08:00 committed by GitHub
parent 585a6e2d40
commit 33422ec921
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 152 additions and 23 deletions

View File

@ -2499,13 +2499,17 @@ const canGoNextPage = computed(() => {
const canJumpLastPage = computed(() => canGoNextPage.value && (hasKnownTotalRowCount.value || allRowsLoaded.value || !!props.tableMeta || !!props.countSql));
const totalRowCountBusy = computed(() => props.totalRowCountLoading === true || manualTotalRowCountLoading.value);
const canCalculateTotalRowCount = computed(() => !isResultsContext.value && !!props.connectionId && (!!props.tableMeta || !!props.countSql));
const showQueryEditReadyBadge = computed(() => isResultsContext.value && hasData.value && !!props.editable && !!props.tableMeta);
const showQueryEditReadyBadge = computed(() => isResultsContext.value && hasData.value && !!props.editable && (!!props.tableMeta || !!props.customSaveHandler));
const queryEditReadyTargetLabel = computed(() => props.tableMeta?.tableName ?? props.customSaveHandler?.targetLabel ?? "");
const showKeylessEditWarning = computed(() => !!props.editable && !!props.tableMeta && canUseKeylessRowPredicate(props.databaseType, props.tableMeta.primaryKeys ?? []));
const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsContext.value);
const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value);
type DataGridTableMeta = NonNullable<typeof props.tableMeta>;
const hiveTableTransactional = ref<boolean | undefined>(undefined);
const canEditExistingRows = computed(() => !!props.customSaveHandler || canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? []));
const customReadonlyColumns = computed(() => new Set((props.customSaveHandler?.readonlyColumns ?? []).map((column) => column.toLowerCase())));
const canInsertRows = computed(() => !props.customSaveHandler || props.customSaveHandler.canInsert !== false);
const canDeleteRows = computed(() => !props.customSaveHandler || props.customSaveHandler.canDelete !== false);
watch(
() => [props.databaseType, props.connectionId, props.database, props.tableMeta?.schema, props.tableMeta?.tableName],
async () => {
@ -2909,8 +2913,9 @@ function canEditRowItem(item: RowItem | undefined): boolean {
function canEditCellItem(item: RowItem | undefined, columnIndex: number): boolean {
if (!canEditRowItem(item) || !canEditColumn(columnIndex)) return false;
const column = props.result.columns[columnIndex] ?? "";
if (customReadonlyColumns.value.has(column.toLowerCase())) return false;
if (!item?.isNew) {
const column = props.result.columns[columnIndex] ?? "";
const sourceColumn = props.sourceColumns?.[columnIndex] ?? column;
if (isClickHouseExistingRowReadonlyColumn(props.databaseType, sourceColumn, props.tableMeta?.primaryKeys ?? [], props.tableMeta?.columns ?? [])) return false;
if (isTdengineExistingRowReadonlyColumn(props.databaseType, column, props.tableMeta?.columns ?? [])) return false;
@ -3010,7 +3015,7 @@ function isDecimalColumnType(dataType: string): boolean {
}
function canDeleteRowItem(item: RowItem | undefined): boolean {
return !!props.editable && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
return !!props.editable && canDeleteRows.value && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
}
function resetInfiniteScrollState() {
@ -3050,6 +3055,7 @@ function onToolbarRollback() {
}
function addRow() {
if (!canInsertRows.value) return;
addEditorRow();
focusAppendedTransposeRecord();
}
@ -4441,7 +4447,7 @@ function onCanvasContext(event: MouseEvent) {
}
const actualColIdx = visibleColumnIndexes.value[hit.visibleColIdx];
if (actualColIdx === undefined) return;
onCellContext(item.id, item.displayIndex, actualColIdx, hit.visibleColIdx);
onCellContext(item.id, item.displayIndex, actualColIdx, hit.visibleColIdx, event);
}
function onCanvasDblClick(event: MouseEvent) {
@ -5355,7 +5361,7 @@ async function onGridKeydown(event: KeyboardEvent) {
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key.toLowerCase() === "n") {
if (props.editable && (props.tableMeta || props.customSaveHandler)) {
if (props.editable && (props.tableMeta || props.customSaveHandler) && canInsertRows.value) {
event.preventDefault();
event.stopPropagation();
addRow();
@ -5981,7 +5987,13 @@ async function copyAlterColumnSql() {
toast(t("grid.copyAlterSqlFailed", { message: e?.message || String(e) }), 5000);
}
}
function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleColIdx: number) {
function clearNativeTextSelection() {
window.getSelection()?.removeAllRanges();
}
function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleColIdx: number, event?: MouseEvent) {
event?.preventDefault();
clearNativeTextSelection();
contextHeaderColumn.value = null;
contextHeaderColumnIndex.value = null;
contextCell.value = { rowId, rowIndex, col: colIdx };
@ -6806,11 +6818,13 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
if (props.editable && contextRowItem.value) {
const labels = rowActionLabels();
items.push({ label: "", separator: true });
items.push({
label: labels.clone,
action: () => (isMultiRow.value ? cloneRows(affectedRowIds()) : cloneRow(contextRowItem.value!.id)),
icon: CopyPlus,
});
if (canInsertRows.value) {
items.push({
label: labels.clone,
action: () => (isMultiRow.value ? cloneRows(affectedRowIds()) : cloneRow(contextRowItem.value!.id)),
icon: CopyPlus,
});
}
if (contextRowItem.value.isDeleted) {
items.push({
label: labels.restore,
@ -7163,7 +7177,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="max-w-sm">
{{ t("grid.queryEditReadyHint", { table: tableMeta?.tableName }) }}
{{ t("grid.queryEditReadyHint", { table: queryEditReadyTargetLabel }) }}
</TooltipContent>
</Tooltip>
<Tooltip v-if="showKeylessEditWarning">
@ -7216,7 +7230,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</TooltipTrigger>
<TooltipContent side="bottom">{{ t("grid.goToColumn") }}</TooltipContent>
</Tooltip>
<Tooltip v-if="editable && (tableMeta || customSaveHandler)">
<Tooltip v-if="editable && (tableMeta || customSaveHandler) && canInsertRows">
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '']" @click="addRow">
<Plus class="data-grid-topbar-action-icon w-3 h-3" />
@ -7999,7 +8013,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@mouseleave="onCellMouseleave(item.displayIndex, col.actualColIdx)"
@dblclick="canEditCellItem(item, col.actualColIdx) && startDomCellEdit(item.id, col.actualColIdx, formatCellCached(item.data[col.actualColIdx], col.actualColIdx), $event)"
:data-visible-col-index="col.visibleColIdx"
@contextmenu="onCellContext(item.id, item.displayIndex, col.actualColIdx, col.visibleColIdx)"
@contextmenu="onCellContext(item.id, item.displayIndex, col.actualColIdx, col.visibleColIdx, $event)"
>
<template v-if="editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx">
<TemporalCellEditor v-if="temporalEditorKindForColumn(col.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(col.actualColIdx)!" @cancel="cancelEdit" @commit="commitGridEdit" />

View File

@ -60,9 +60,12 @@ import { tableMetaForDataTab } from "@/lib/tableDataTabMeta";
import { formatShortcut } from "@/lib/shortcutRegistry";
import { effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
import { chartableColumnIndexes } from "@/lib/chartData";
import * as api from "@/lib/api";
import { buildMongoUpdateDocument, formatMongoShellLiteral, type MongoInputValue } from "@/lib/mongoDocumentValues";
import type { SqlExecutionOverride } from "@/lib/sqlExecutionTarget";
import type { DataGridSortMode } from "@/lib/dataGridSort";
import { useTabScroll } from "@/composables/useTabScroll";
import type { CustomSaveHandler } from "@/composables/useDataGridEditor";
import type { QueryTab, ConnectionConfig, TableInfoTab, TreeNode, VectorCollectionMeta } from "@/types/database";
import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sqlFormatter";
@ -279,6 +282,60 @@ const hasTabularResult = computed(() => {
return visibleResultItems.value.length > 0;
});
const canShowResultOutput = computed(() => hasTabularResult.value || props.activeTab.isExecuting);
type MongoQueryGridChanges = {
dirtyRows: Map<number, Map<number, MongoInputValue>>;
deletedRows: Set<number>;
newRows: MongoInputValue[][];
columns: string[];
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 {
return `db.getCollection(${JSON.stringify(collection)})`;
}
const mongoQueryResultSaveHandler = computed<CustomSaveHandler | undefined>(() => {
const tab = props.activeTab;
const target = tab.mongoEditTarget;
if (tab.mode !== "query" || activeEffectiveDatabaseType.value !== "mongodb" || !target || !tab.connectionId || !tab.database || !tab.result) return undefined;
if (!tab.result.columns.includes(target.idColumn)) return undefined;
const save: CustomSaveHandler["save"] = async (changes: MongoQueryGridChanges) => {
if (changes.newRows.length > 0 || changes.deletedRows.size > 0) {
throw new Error("MongoDB query result editing only supports updating existing rows.");
}
const idColIdx = changes.columns.indexOf(target.idColumn);
if (idColIdx < 0) throw new Error("No _id column");
for (const [rowIdx, dirtyCols] of changes.dirtyRows) {
const row = changes.rows[rowIdx];
const id = row?.[idColIdx];
if (id === null || id === undefined || String(id).trim() === "") continue;
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns);
if (Object.keys(updateDoc).length === 0) continue;
await api.mongoUpdateDocument(tab.connectionId, tab.database, target.collection, String(id), JSON.stringify(updateDoc));
}
};
const preview: CustomSaveHandler["preview"] = async (changes: MongoQueryGridChanges) => {
const idColIdx = changes.columns.indexOf(target.idColumn);
if (idColIdx < 0) return [];
const stmts: string[] = [];
for (const [rowIdx, dirtyCols] of changes.dirtyRows) {
const row = changes.rows[rowIdx];
const id = row?.[idColIdx];
if (id === null || id === undefined || String(id).trim() === "") continue;
const updateDoc = buildMongoUpdateDocument(dirtyCols, changes.columns);
if (Object.keys(updateDoc).length === 0) continue;
stmts.push(`${mongoCollectionExpression(target.collection)}.updateOne({_id: ${mongoIdPreview(id)}}, ${formatMongoShellLiteral(updateDoc)})`);
}
return stmts;
};
return { save, preview, canInsert: false, canDelete: false, readonlyColumns: [target.idColumn], targetLabel: target.collection };
});
const resultsPaneOpen = ref(false);
const resultsPaneSize = ref(Number(safeLocalStorageGet("dbx-results-pane-size")) || DEFAULT_QUERY_RESULTS_PANE_SIZE);
const editorPaneSize = computed(() => (resultsPaneOpen.value ? 100 - resultsPaneSize.value : 100));
@ -821,8 +878,9 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
:initial-order-by-input="activeTab.orderByInput"
:sql="activeTab.lastExecutedSql || activeTab.sql"
:loading="activeTab.isExecuting"
:editable="!!activeTab.queryAnalysis"
:editable="!!activeTab.queryAnalysis || !!mongoQueryResultSaveHandler"
:source-columns="activeTab.querySourceColumns"
:custom-save-handler="mongoQueryResultSaveHandler"
context="results"
:database-type="activeEffectiveDatabaseType"
:connection-id="activeTab.connectionId"

View File

@ -34,6 +34,10 @@ type GridScrollerRef =
export interface CustomSaveHandler {
save: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<void>;
preview?: (changes: { dirtyRows: Map<number, Map<number, CellValue>>; newRows: CellValue[][]; deletedRows: Set<number>; columns: string[]; rows: CellValue[][] }) => Promise<string[]>;
canInsert?: boolean;
canDelete?: boolean;
readonlyColumns?: string[];
targetLabel?: string;
}
export interface UseDataGridEditorOptions {
@ -834,6 +838,17 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
options.emit("reload", sql.value, searchText.value, options.currentWhereInput.value, orderByInput.value.trim() || undefined, pageSize.value, (currentPage.value - 1) * pageSize.value);
}
function applyDirtyRowsToResult() {
for (const [sourceIndex, changes] of dirtyRows.value) {
const row = result.value.rows[sourceIndex];
if (row) {
for (const [colIdx, value] of changes) {
row[colIdx] = value;
}
}
}
}
async function saveChanges() {
saveError.value = "";
isSaving.value = true;
@ -853,6 +868,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
isSaving.value = false;
return;
}
applyDirtyRowsToResult();
dirtyRows.value.clear();
newRows.value = [];
deletedRows.value.clear();
@ -931,14 +947,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
} catch (e) {
console.warn("[DBX] failed to record data grid history", e);
}
for (const [sourceIndex, changes] of dirtyRows.value) {
const row = result.value.rows[sourceIndex];
if (row) {
for (const [colIdx, value] of changes) {
row[colIdx] = value;
}
}
}
applyDirtyRowsToResult();
dirtyRows.value.clear();
newRows.value = [];
deletedRows.value.clear();

View File

@ -41,6 +41,7 @@ export interface SavedOpenTab {
objectBrowser?: QueryTab["objectBrowser"];
objectSource?: QueryTab["objectSource"];
tableMeta?: QueryTab["tableMeta"];
mongoEditTarget?: QueryTab["mongoEditTarget"];
resultEvicted?: boolean;
resultCacheKey?: string;
resultRuns?: SavedQueryResultRun[];
@ -89,6 +90,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
objectBrowser: tab.objectBrowser,
objectSource: tab.objectSource,
tableMeta: tab.tableMeta,
...(tab.mongoEditTarget !== undefined ? { mongoEditTarget: tab.mongoEditTarget } : {}),
...(tab.mode !== "data" && tab.resultEvicted ? { resultEvicted: true } : {}),
...(tab.mode !== "data" && tab.resultEvicted && tab.resultCacheKey !== undefined ? { resultCacheKey: tab.resultCacheKey } : {}),
...(tab.mode === "query" && tab.resultRuns?.length

View File

@ -20,6 +20,7 @@ export interface TabResultSnapshot {
queryAnalysis?: QueryTab["queryAnalysis"];
querySourceColumns?: QueryTab["querySourceColumns"];
queryEditabilityReason?: QueryTab["queryEditabilityReason"];
mongoEditTarget?: QueryTab["mongoEditTarget"];
tableMeta?: QueryTab["tableMeta"];
resultPageSql?: string;
resultPageLimit?: number;
@ -367,6 +368,7 @@ export function buildTabResultSnapshot(tab: QueryTab): TabResultSnapshot | undef
queryAnalysis: tab.queryAnalysis ? clonePlain(tab.queryAnalysis) : undefined,
querySourceColumns: tab.querySourceColumns ? [...tab.querySourceColumns] : undefined,
queryEditabilityReason: tab.queryEditabilityReason,
mongoEditTarget: tab.mongoEditTarget ? clonePlain(tab.mongoEditTarget) : undefined,
tableMeta: tab.tableMeta ? clonePlain(tab.tableMeta) : undefined,
resultPageSql: tab.resultPageSql,
resultPageLimit: tab.resultPageLimit,

View File

@ -254,6 +254,7 @@ export const useQueryStore = defineStore("query", () => {
tab.queryAnalysis = undefined;
tab.querySourceColumns = undefined;
tab.queryEditabilityReason = undefined;
tab.mongoEditTarget = undefined;
if (tab.mode === "query") tab.tableMeta = undefined;
tab.resultEvicted = options.evicted ? true : undefined;
tab.resultCacheState = options.evicted ? tab.resultCacheState : undefined;
@ -297,6 +298,7 @@ export const useQueryStore = defineStore("query", () => {
tab.queryAnalysis = run.queryAnalysis;
tab.querySourceColumns = run.querySourceColumns;
tab.queryEditabilityReason = run.queryEditabilityReason;
tab.mongoEditTarget = run.mongoEditTarget;
tab.tableMeta = run.tableMeta;
touchResult(tab);
}
@ -416,6 +418,7 @@ export const useQueryStore = defineStore("query", () => {
queryAnalysis: tab.queryAnalysis,
querySourceColumns: tab.querySourceColumns,
queryEditabilityReason: tab.queryEditabilityReason,
mongoEditTarget: tab.mongoEditTarget,
tableMeta: tab.tableMeta,
};
persistResultRun(tab, run);
@ -463,6 +466,7 @@ export const useQueryStore = defineStore("query", () => {
queryAnalysis: tab.queryAnalysis,
querySourceColumns: tab.querySourceColumns,
queryEditabilityReason: tab.queryEditabilityReason,
mongoEditTarget: tab.mongoEditTarget,
tableMeta: tab.tableMeta,
};
persistResultRun(tab, run);
@ -555,6 +559,7 @@ export const useQueryStore = defineStore("query", () => {
objectBrowser: t.objectBrowser,
objectSource: t.objectSource,
tableMeta: t.tableMeta,
mongoEditTarget: t.mongoEditTarget,
resultEvicted: t.resultEvicted,
resultCacheKey: t.resultCacheKey,
})),
@ -1222,6 +1227,7 @@ export const useQueryStore = defineStore("query", () => {
tab.queryAnalysis = patch.queryAnalysis;
tab.querySourceColumns = patch.querySourceColumns;
tab.queryEditabilityReason = patch.queryEditabilityReason;
tab.mongoEditTarget = undefined;
tab.tableMeta = patch.tableMeta;
}
@ -1540,6 +1546,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
@ -1598,6 +1605,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = current.result.columns.includes("_id") ? { collection: mongoFind.collection, idColumn: "_id" } : undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
@ -1624,6 +1632,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
@ -1657,6 +1666,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
@ -1684,6 +1694,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
@ -1725,6 +1736,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
@ -1746,6 +1758,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = undefined;
current.tableMeta = undefined;
current.resultBaseSql = options?.resultBaseSql ?? sql;
current.resultSortedSql = options?.resultSortedSql;
@ -1871,6 +1884,7 @@ export const useQueryStore = defineStore("query", () => {
current.queryAnalysis = undefined;
current.querySourceColumns = undefined;
current.queryEditabilityReason = undefined;
current.mongoEditTarget = undefined;
if (current.mode !== "data") current.tableMeta = undefined;
current.resultBaseSql = queryBaseSql;
current.resultSortedSql = options?.resultSortedSql;
@ -2065,6 +2079,7 @@ export const useQueryStore = defineStore("query", () => {
tab.queryAnalysis = undefined;
tab.querySourceColumns = undefined;
tab.queryEditabilityReason = undefined;
tab.mongoEditTarget = undefined;
syncActiveResultRunFromDisplayed(tab);
}
@ -2110,6 +2125,7 @@ export const useQueryStore = defineStore("query", () => {
tab.queryAnalysis = snapshot.queryAnalysis;
tab.querySourceColumns = snapshot.querySourceColumns;
tab.queryEditabilityReason = snapshot.queryEditabilityReason;
tab.mongoEditTarget = snapshot.mongoEditTarget;
tab.tableMeta = snapshot.tableMeta;
tab.resultPageSql = snapshot.resultPageSql;
tab.resultPageLimit = snapshot.resultPageLimit;

View File

@ -430,6 +430,7 @@ export interface QueryResultRun {
queryAnalysis?: QueryTab["queryAnalysis"];
querySourceColumns?: QueryTab["querySourceColumns"];
queryEditabilityReason?: QueryTab["queryEditabilityReason"];
mongoEditTarget?: QueryTab["mongoEditTarget"];
tableMeta?: QueryTab["tableMeta"];
}
@ -652,6 +653,10 @@ export interface QueryTab {
};
querySourceColumns?: Array<string | undefined>;
queryEditabilityReason?: "not-select" | "cte" | "set-operation" | "aggregation" | "external-source" | "complex-source" | "computed-columns" | "no-table" | "no-primary-key" | "primary-key-not-returned" | "aliased-columns" | "metadata-unavailable";
mongoEditTarget?: {
collection: string;
idColumn: "_id";
};
resultEvicted?: boolean;
whereInput?: string;
previewSql?: string;

View File

@ -12,6 +12,7 @@ import {
parseMongoGetIndexesCommand,
parseMongoWriteCommand,
} from "../../apps/desktop/src/lib/mongoShellCommand.ts";
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral } from "../../apps/desktop/src/lib/mongoDocumentValues.ts";
test("parseMongoFindCommand parses db collection find with an empty JSON filter", () => {
assert.deepEqual(parseMongoFindCommand("db.users.find({})"), {
@ -197,3 +198,25 @@ test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => {
assert.equal(result.execution_time_ms, 5);
assert.equal(result.truncated, true);
});
test("buildMongoUpdateDocument ignores _id and preserves typed values", () => {
const changes = new Map<number, string | number | boolean | null>([
[0, "other-id"],
[1, "42"],
[2, '{"role":"admin"}'],
[3, null],
]);
const update = buildMongoDocumentUpdate(changes, ["_id", "age", "profile", "nickname"]);
assert.deepEqual(update, {
$set: {
age: 42,
profile: { role: "admin" },
},
$unset: {
nickname: "",
},
});
assert.equal(formatMongoDocumentShellLiteral(update), '{"$set":{"age":42,"profile":{"role":"admin"}},"$unset":{"nickname":""}}');
});