feat(mongodb): 支持文档可视化编辑与删除

- 前端表格预览使用 _id 构建 MongoDB 行定位,并隐藏 typed ObjectID locator

- 后端 ApplyChanges 支持 MongoDB 更新、单删和批量删除,区分 ObjectID 与字符串 _id

- 补充 DataViewer、DataGrid 与双版本 Mongo driver 回归测试

Refs #458
This commit is contained in:
Syngnat
2026-05-13 21:48:14 +08:00
parent 2ad2f26b2b
commit 01eb2c25e0
10 changed files with 547 additions and 52 deletions

View File

@@ -328,6 +328,63 @@ describe('DataGrid commit change set', () => {
});
});
it('uses MongoDB _id as the locator and keeps _id out of update values', () => {
const result = buildDataGridCommitChangeSet({
addedRows: [{
[GONAVI_ROW_KEY]: 'new-1',
_id: '507f1f77bcf86cd799439013',
__gonavi_mongodb_id_locator__: { $oid: '507f1f77bcf86cd799439013' },
name: 'insert-name',
}],
modifiedRows: {
'row-1': {
[GONAVI_ROW_KEY]: 'row-1',
_id: '507f1f77bcf86cd799439999',
__gonavi_mongodb_id_locator__: '507f1f77bcf86cd799439999',
name: 'new-name',
},
},
deletedRowKeys: new Set(['row-2']),
data: [
{
[GONAVI_ROW_KEY]: 'row-1',
_id: '507f1f77bcf86cd799439011',
__gonavi_mongodb_id_locator__: { $oid: '507f1f77bcf86cd799439011' },
name: 'old-name',
},
{
[GONAVI_ROW_KEY]: 'row-2',
_id: '507f1f77bcf86cd799439012',
__gonavi_mongodb_id_locator__: '507f1f77bcf86cd799439012',
name: 'to-delete',
},
],
editLocator: {
strategy: 'primary-key',
columns: ['_id'],
valueColumns: ['__gonavi_mongodb_id_locator__'],
hiddenColumns: ['__gonavi_mongodb_id_locator__'],
writableColumns: {
name: 'name',
},
readOnly: false,
},
visibleColumnNames: ['_id', 'name'],
rowKeyToString,
normalizeCommitCellValue: normalizeValue,
shouldCommitColumn: commitColumnGuard,
});
expect(result).toEqual({
ok: true,
changes: {
inserts: [{ name: 'insert-name' }],
updates: [{ keys: { _id: { $oid: '507f1f77bcf86cd799439011' } }, values: { name: 'new-name' } }],
deletes: [{ _id: '507f1f77bcf86cd799439012' }],
},
});
});
it('fails closed when no safe locator is available', () => {
const result = buildDataGridCommitChangeSet({
addedRows: [],

View File

@@ -1453,6 +1453,11 @@ const DataGrid: React.FC<DataGridProps> = ({
const [dataPanelIsJson, setDataPanelIsJson] = useState(false);
const dataPanelDirtyRef = useRef(false);
const dataPanelOriginalRef = useRef('');
const focusedCellWritable = useMemo(() => (
canModifyData &&
!!focusedCellInfo &&
isWritableResultColumn(focusedCellInfo.dataIndex, effectiveEditLocator)
), [canModifyData, focusedCellInfo, effectiveEditLocator]);
const [rowEditorOpen, setRowEditorOpen] = useState(false);
const [rowEditorRowKey, setRowEditorRowKey] = useState<string>('');
const rowEditorBaseRawRef = useRef<Record<string, any>>({});
@@ -3020,6 +3025,15 @@ const DataGrid: React.FC<DataGridProps> = ({
return;
}
const writablePatchValues = Object.fromEntries(
Object.entries(copiedCellPatch.values)
.filter(([colName]) => isWritableResultColumn(colName, effectiveEditLocator))
);
if (Object.keys(writablePatchValues).length === 0) {
void message.info('没有可粘贴的可编辑字段');
return;
}
const targetKeySet = new Set<string>();
const selectedKeys = selectedRowKeysRef.current;
if (selectedKeys.length > 0) {
@@ -3060,7 +3074,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const addedRow = addedRowMap.get(targetRowKey);
const baseRow = baseRowMap.get(targetRowKey);
Object.entries(copiedCellPatch.values).forEach(([colName, nextValue]) => {
Object.entries(writablePatchValues).forEach(([colName, nextValue]) => {
let currentValue: any;
if (addedRow) {
@@ -3112,10 +3126,14 @@ const DataGrid: React.FC<DataGridProps> = ({
void message.success(`已粘贴到 ${patchesByRow.size} 行,共 ${updatedCellCount} 个单元格`);
setCellContextMenu(prev => ({ ...prev, visible: false }));
}, [copiedCellPatch, addedRows, modifiedRows, rowKeyStr]);
}, [copiedCellPatch, addedRows, modifiedRows, rowKeyStr, effectiveEditLocator]);
// 批量填充到选中行
const handleBatchFillToSelected = useCallback((sourceRecord: Item, dataIndex: string) => {
if (!isWritableResultColumn(dataIndex, effectiveEditLocator)) {
void message.info('当前字段不可编辑');
return;
}
const sourceValue = sourceRecord[dataIndex];
const selKeys = selectedRowKeysRef.current;
@@ -3170,7 +3188,7 @@ const DataGrid: React.FC<DataGridProps> = ({
void message.success(`已填充 ${updatedCount}`);
setCellContextMenu(prev => ({ ...prev, visible: false }));
}, [addedRows, rowKeyStr]);
}, [addedRows, rowKeyStr, effectiveEditLocator]);
const displayData = useMemo(() => {
return [...data, ...addedRows];
@@ -3431,6 +3449,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const changedFields: Record<string, any> = {};
for (const col of Object.keys(row)) {
if (col === GONAVI_ROW_KEY) continue;
if (!isWritableResultColumn(col, effectiveEditLocator)) continue;
if (!isCellValueEqualForDiff(originalRow[col], row[col])) {
changedFields[col] = row[col];
}
@@ -3464,10 +3483,14 @@ const DataGrid: React.FC<DataGridProps> = ({
});
setModifiedRows(prev => ({ ...prev, [keyStr]: row }));
}
}, [addedRows, data, rowKeyStr]);
}, [addedRows, data, rowKeyStr, deletedRowKeys, effectiveEditLocator]);
const handleDataPanelSave = useCallback(() => {
if (!focusedCellInfo) return;
if (!focusedCellWritable) {
void message.info('当前字段不可编辑');
return;
}
// 与 updateFocusedCell 设置的原始值比较,避免幽灵变更
if (dataPanelValue === dataPanelOriginalRef.current) {
dataPanelDirtyRef.current = false;
@@ -3479,16 +3502,26 @@ const DataGrid: React.FC<DataGridProps> = ({
dataPanelOriginalRef.current = dataPanelValue;
dataPanelDirtyRef.current = false;
void message.success('已保存');
}, [focusedCellInfo, dataPanelValue, handleCellSave]);
}, [focusedCellInfo, focusedCellWritable, dataPanelValue, handleCellSave]);
const handleCellSetNull = useCallback(() => {
if (!cellContextMenu.record) return;
if (!isWritableResultColumn(cellContextMenu.dataIndex, effectiveEditLocator)) {
void message.info('当前字段不可编辑');
setCellContextMenu(prev => ({ ...prev, visible: false }));
return;
}
handleCellSave({ ...cellContextMenu.record, [cellContextMenu.dataIndex]: null });
setCellContextMenu(prev => ({ ...prev, visible: false }));
}, [cellContextMenu, handleCellSave]);
}, [cellContextMenu, handleCellSave, effectiveEditLocator]);
const handleCellEditorSave = useCallback(() => {
if (!cellEditorMeta) return;
if (!isWritableResultColumn(cellEditorMeta.dataIndex, effectiveEditLocator)) {
void message.info('当前字段不可编辑');
closeCellEditor();
return;
}
const apply = cellEditorApplyRef.current;
if (apply) {
apply(cellEditorValue);
@@ -3498,7 +3531,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const nextRow: any = { ...cellEditorMeta.record, [cellEditorMeta.dataIndex]: cellEditorValue };
handleCellSave(nextRow);
closeCellEditor();
}, [cellEditorMeta, cellEditorValue, handleCellSave, closeCellEditor]);
}, [cellEditorMeta, cellEditorValue, handleCellSave, closeCellEditor, effectiveEditLocator]);
const handleFormatJsonInEditor = useCallback(() => {
if (!cellEditorIsJson) return;
@@ -3666,7 +3699,7 @@ const DataGrid: React.FC<DataGridProps> = ({
rowEditorForm.setFieldsValue(formMap);
setRowEditorRowKey(keyStr);
setRowEditorOpen(true);
}, [canModifyData, mergedDisplayData, data, addedRows, displayColumnNames, rowEditorForm, rowKeyStr, columnMetaMap, columnMetaMapByLowerName]);
}, [canModifyData, mergedDisplayData, data, addedRows, visibleColumnNames, rowEditorForm, rowKeyStr, columnMetaMap, columnMetaMapByLowerName]);
const openCurrentViewRowEditor = useCallback(() => {
if (!canModifyData) return;
@@ -3789,15 +3822,16 @@ const DataGrid: React.FC<DataGridProps> = ({
}
const keyStr = rowKeyStr(rowKey);
const normalizedNext: Record<string, any> = {};
let hasAnyVisibleChange = false;
let hasAnyWritableChange = false;
visibleColumnNames.forEach((col) => {
if (!isWritableResultColumn(col, effectiveEditLocator)) return;
const currentVal = (currentRow as any)?.[col];
const editedVal = Object.prototype.hasOwnProperty.call(nextItem, col) ? (nextItem as any)[col] : currentVal;
if (!isJsonViewValueEqual(currentVal, editedVal)) hasAnyVisibleChange = true;
if (!isJsonViewValueEqual(currentVal, editedVal)) hasAnyWritableChange = true;
normalizedNext[col] = coerceJsonEditorValueForStorage(currentVal, editedVal);
});
if (!hasAnyVisibleChange) {
if (!hasAnyWritableChange) {
continue;
}
@@ -3810,6 +3844,7 @@ const DataGrid: React.FC<DataGridProps> = ({
if (!originalRow) continue;
const patch: Record<string, any> = {};
visibleColumnNames.forEach((col) => {
if (!isWritableResultColumn(col, effectiveEditLocator)) return;
const prevVal = (originalRow as any)?.[col];
const nextVal = normalizedNext[col];
if (!isCellValueEqualForDiff(prevVal, nextVal)) patch[col] = nextVal;
@@ -3836,10 +3871,14 @@ const DataGrid: React.FC<DataGridProps> = ({
setJsonEditorOpen(false);
void message.success("JSON 修改已应用到当前结果集,可继续“提交事务”");
}, [canModifyData, jsonEditorValue, mergedDisplayData, addedRows, rowKeyStr, data, displayColumnNames]);
}, [canModifyData, jsonEditorValue, mergedDisplayData, addedRows, rowKeyStr, data, visibleColumnNames, effectiveEditLocator]);
const openRowEditorFieldEditor = useCallback((dataIndex: string) => {
if (!dataIndex) return;
if (!isWritableResultColumn(dataIndex, effectiveEditLocator)) {
void message.info('当前字段不可编辑');
return;
}
const val = rowEditorForm.getFieldValue(dataIndex);
openCellEditor(
{ [dataIndex]: val ?? '' },
@@ -3847,7 +3886,7 @@ const DataGrid: React.FC<DataGridProps> = ({
dataIndex,
(nextVal) => rowEditorForm.setFieldsValue({ [dataIndex]: nextVal }),
);
}, [rowEditorForm, openCellEditor]);
}, [rowEditorForm, openCellEditor, effectiveEditLocator]);
const applyRowEditor = useCallback(() => {
const keyStr = rowEditorRowKey;
@@ -3859,6 +3898,7 @@ const DataGrid: React.FC<DataGridProps> = ({
// 日期时间类型: 将 dayjs 对象转回格式化字符串
const convertedValues: Record<string, any> = {};
Object.entries(values).forEach(([col, val]) => {
if (!isWritableResultColumn(col, effectiveEditLocator)) return;
if (val && dayjs.isDayjs(val)) {
const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
const rowPickerType = getTemporalPickerType(colMeta?.type);
@@ -3875,6 +3915,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const baseRawMap = rowEditorBaseRawRef.current || {};
const patch: Record<string, any> = {};
visibleColumnNames.forEach((col) => {
if (!isWritableResultColumn(col, effectiveEditLocator)) return;
let nextVal = values[col];
// 日期时间类型: 将 dayjs 对象转回格式化字符串
if (nextVal && dayjs.isDayjs(nextVal)) {
@@ -3894,7 +3935,7 @@ const DataGrid: React.FC<DataGridProps> = ({
});
closeRowEditor();
}, [rowEditorRowKey, rowEditorForm, addedRows, visibleColumnNames, rowKeyStr, closeRowEditor]);
}, [rowEditorRowKey, rowEditorForm, addedRows, visibleColumnNames, rowKeyStr, closeRowEditor, effectiveEditLocator, columnMetaMap, columnMetaMapByLowerName]);
const enableVirtual = viewMode === 'table';
@@ -4071,7 +4112,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const copiedRows = buildCopiedRowsForPaste({
rows: mergedDisplayData as Array<Record<string, any>>,
selectedRowKeys,
columnNames: displayOutputColumnNames,
columnNames: displayOutputColumnNames.filter((columnName) => isWritableResultColumn(columnName, effectiveEditLocator)),
rowKeyField: GONAVI_ROW_KEY,
rowKeyToString: rowKeyStr,
});
@@ -4082,7 +4123,7 @@ const DataGrid: React.FC<DataGridProps> = ({
setCopiedRowsForPaste(copiedRows);
void message.success(`已复制 ${copiedRows.length} 行,可粘贴为新增行`);
}, [selectedRowKeys, mergedDisplayData, displayOutputColumnNames, rowKeyStr]);
}, [selectedRowKeys, mergedDisplayData, displayOutputColumnNames, rowKeyStr, effectiveEditLocator]);
const handlePasteCopiedRowsAsNew = useCallback(() => {
if (copiedRowsForPaste.length === 0) {
@@ -4092,7 +4133,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const nextRows = buildPastedRowsFromCopiedRows({
rows: copiedRowsForPaste,
columnNames: displayOutputColumnNames,
columnNames: displayOutputColumnNames.filter((columnName) => isWritableResultColumn(columnName, effectiveEditLocator)),
rowKeyField: GONAVI_ROW_KEY,
createRowKey: (index) => {
pastedRowSequenceRef.current += 1;
@@ -4108,7 +4149,7 @@ const DataGrid: React.FC<DataGridProps> = ({
setAddedRows(prev => [...prev, ...nextRows]);
setSelectedRowKeys(nextRows.map(row => row[GONAVI_ROW_KEY]));
void message.success(`已粘贴 ${nextRows.length} 行为新增行,请检查后提交事务`);
}, [copiedRowsForPaste, displayOutputColumnNames]);
}, [copiedRowsForPaste, displayOutputColumnNames, effectiveEditLocator]);
const handleDeleteSelected = () => {
const addedKeysToRemove: string[] = [];
@@ -6151,6 +6192,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const colMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
const rowPickerType = getTemporalPickerType(colMeta?.type);
const isRowDateTimeField = !!rowPickerType && !(/^0{4}-0{2}-0{2}/.test(String(sample || '')));
const isWritable = isWritableResultColumn(col, effectiveEditLocator);
return (
<Form.Item key={col} label={col} style={{ marginBottom: 12 }}>
@@ -6163,6 +6205,7 @@ const DataGrid: React.FC<DataGridProps> = ({
format={TEMPORAL_FORMATS[rowPickerType]}
placeholder={placeholder}
needConfirm={false}
disabled={!isWritable}
/>
) : rowPickerType === 'datetime' ? (
<DatePicker
@@ -6171,6 +6214,7 @@ const DataGrid: React.FC<DataGridProps> = ({
format={TEMPORAL_FORMATS[rowPickerType]}
placeholder={placeholder}
needConfirm
disabled={!isWritable}
/>
) : (
<DatePicker
@@ -6179,6 +6223,7 @@ const DataGrid: React.FC<DataGridProps> = ({
picker={rowPickerType as any}
placeholder={placeholder}
needConfirm={false}
disabled={!isWritable}
/>
)
) : useArea ? (
@@ -6186,12 +6231,13 @@ const DataGrid: React.FC<DataGridProps> = ({
style={{ flex: 1 }}
autoSize={{ minRows: isJson ? 4 : 1, maxRows: 10 }}
placeholder={placeholder}
disabled={!isWritable}
/>
) : (
<Input style={{ flex: 1 }} placeholder={placeholder} />
<Input style={{ flex: 1 }} placeholder={placeholder} disabled={!isWritable} />
)}
</Form.Item>
<Button size="small" onClick={() => openRowEditorFieldEditor(col)} title="弹窗编辑">...</Button>
<Button size="small" onClick={() => openRowEditorFieldEditor(col)} title="弹窗编辑" disabled={!isWritable}>...</Button>
</div>
</Form.Item>
);
@@ -6488,7 +6534,7 @@ const DataGrid: React.FC<DataGridProps> = ({
{dataPanelIsJson && (
<Button size="small" onClick={handleDataPanelFormatJson}> JSON</Button>
)}
{canModifyData && focusedCellInfo && (
{focusedCellWritable && (
<Button size="small" type="primary" onClick={handleDataPanelSave}></Button>
)}
</div>
@@ -6512,7 +6558,7 @@ const DataGrid: React.FC<DataGridProps> = ({
fontSize: 13,
tabSize: 2,
automaticLayout: true,
readOnly: !canModifyData,
readOnly: !focusedCellWritable,
lineNumbers: 'off',
glyphMargin: false,
folding: false,

View File

@@ -154,6 +154,70 @@ describe('DataViewer safe editing locator', () => {
renderer.unmount();
});
it('enables MongoDB table preview editing through the _id locator', async () => {
storeState.connections[0].config.type = 'mongodb';
storeState.connections[0].config.database = 'app';
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['_id', '__gonavi_mongodb_id_locator__', 'name', 'age'],
data: [{
_id: '507f1f77bcf86cd799439011',
__gonavi_mongodb_id_locator__: { $oid: '507f1f77bcf86cd799439011' },
name: 'old-name',
age: 18,
}],
});
const renderer = await renderAndReload(createTab({ id: 'tab-mongo', dbName: 'app', tableName: 'users', title: 'users' }));
expect(backendApp.DBGetColumns).not.toHaveBeenCalled();
expect(backendApp.DBGetIndexes).not.toHaveBeenCalled();
expect(dataGridState.latestProps?.pkColumns).toEqual(['_id']);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'primary-key',
columns: ['_id'],
valueColumns: ['__gonavi_mongodb_id_locator__'],
hiddenColumns: ['__gonavi_mongodb_id_locator__'],
writableColumns: {
name: 'name',
age: 'age',
},
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
const mongoFindCall = backendApp.DBQuery.mock.calls.find((call: any[]) => String(call[2] || '').includes('"find":"users"'));
expect(mongoFindCall).toBeTruthy();
expect(JSON.parse(String(mongoFindCall?.[2] || '{}'))).toMatchObject({
find: 'users',
sort: { _id: 1 },
__gonaviIncludeObjectIDLocator: true,
});
renderer.unmount();
});
it('keeps MongoDB results read-only when _id is missing', async () => {
storeState.connections[0].config.type = 'mongodb';
storeState.connections[0].config.database = 'app';
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['name'],
data: [{ name: 'orphan-doc' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-mongo-no-id', dbName: 'app', tableName: 'users', title: 'users' }));
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'none',
readOnly: true,
reason: 'MongoDB 结果集中缺少 _id无法安全提交修改。',
});
expect(dataGridState.latestProps?.readOnly).toBe(true);
expect(messageApi.warning).toHaveBeenCalledWith('集合 app.users 保持只读MongoDB 结果集中缺少 _id无法安全提交修改。');
renderer.unmount();
});
it('uses hidden Oracle ROWID when no primary or unique key is available', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,

View File

@@ -108,6 +108,41 @@ const getTableColumnNames = (columns: ColumnDefinition[] | undefined): string[]
.filter(Boolean)
);
const MONGODB_ID_COLUMN = '_id';
const MONGODB_ID_LOCATOR_COLUMN = '__gonavi_mongodb_id_locator__';
const buildMongoDataViewerEditLocator = (resultColumns: string[]): EditRowLocator => {
const columns = (resultColumns || [])
.map((column) => String(column || '').trim())
.filter(Boolean);
const idColumn = columns.find((column) => column.toLowerCase() === MONGODB_ID_COLUMN);
if (!idColumn) {
return buildDataViewerReadOnlyLocator('MongoDB 结果集中缺少 _id无法安全提交修改。');
}
const locatorValueColumn = columns.find((column) => column === MONGODB_ID_LOCATOR_COLUMN) || idColumn;
const writableColumns: Record<string, string> = {};
columns.forEach((column) => {
const normalized = String(column || '').trim();
if (
!normalized ||
normalized === GONAVI_ROW_KEY ||
normalized === MONGODB_ID_LOCATOR_COLUMN ||
normalized.toLowerCase() === MONGODB_ID_COLUMN
) return;
writableColumns[normalized] = normalized;
});
return {
strategy: 'primary-key',
columns: [MONGODB_ID_COLUMN],
valueColumns: [locatorValueColumn],
hiddenColumns: locatorValueColumn === MONGODB_ID_LOCATOR_COLUMN ? [MONGODB_ID_LOCATOR_COLUMN] : undefined,
writableColumns,
readOnly: false,
};
};
const resolveDataViewerOrderFallbackColumns = (locator: EditRowLocator | undefined, pkColumns: string[]): string[] => {
if (locator && !locator.readOnly && locator.strategy !== 'oracle-rowid') {
return locator.valueColumns.length > 0 ? locator.valueColumns : locator.columns;
@@ -506,6 +541,9 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAct
let pkColumnsForQuery = pkColumns;
let editLocatorForQuery = editLocator;
if (isMongoDB && !forceReadOnly && tableName) {
pkColumnsForQuery = [MONGODB_ID_COLUMN];
}
if (!isMongoDB && !forceReadOnly && tableName) {
const locatorKey = `${tab.connectionId}|${dbTypeLower}|${dbName}|${tableName}`;
if (pkKeyRef.current !== locatorKey || !editLocatorForQuery) {
@@ -602,13 +640,14 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAct
let clickHouseReverseHasMore = false;
let sql = '';
if (isMongoDB) {
const mongoSort = buildMongoSort(sortInfo, pkColumns);
const mongoSort = buildMongoSort(sortInfo, pkColumnsForQuery);
sql = buildMongoFindCommand({
collection: tableName,
filter: mongoFilter || {},
sort: mongoSort,
limit: size + 1,
skip: offset,
includeObjectIDLocator: true,
});
} else {
const baseSql = buildDataViewerBaseSelectSQL(dbType, tableName, whereSQL, editLocatorForQuery);
@@ -740,6 +779,16 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAct
fieldNames = Object.keys(resultData[0]);
}
if (fetchSeqRef.current !== seq) return;
if (isMongoDB && !forceReadOnly && tableName) {
const nextLocator = buildMongoDataViewerEditLocator(fieldNames);
pkColumnsForQuery = nextLocator.readOnly ? [] : [MONGODB_ID_COLUMN];
editLocatorForQuery = nextLocator;
setPkColumns(pkColumnsForQuery);
setEditLocator(nextLocator);
if (nextLocator.readOnly && resultData.length > 0) {
message.warning(`集合 ${formatDataViewerTableName(dbName, tableName)} 保持只读:${nextLocator.reason || '当前结果没有可用的安全行定位方式,无法提交修改。'}`);
}
}
setColumnNames(fieldNames);
resultData.forEach((row: any, i: number) => {
if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = `row-${offset + i}`;

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { applyMongoQueryAutoLimit, convertMongoShellToJsonCommand } from './mongodb';
import { applyMongoQueryAutoLimit, buildMongoFindCommand, convertMongoShellToJsonCommand } from './mongodb';
const parseCommand = (command: string | undefined) => JSON.parse(command || '{}');
@@ -120,3 +120,17 @@ describe('applyMongoQueryAutoLimit', () => {
expect(applyMongoQueryAutoLimit('db.users.find({})', 500).applied).toBe(false);
});
});
describe('buildMongoFindCommand', () => {
it('marks DataViewer Mongo find commands to include typed _id locator', () => {
expect(parseCommand(buildMongoFindCommand({
collection: 'users',
filter: {},
includeObjectIDLocator: true,
}))).toEqual({
find: 'users',
filter: {},
__gonaviIncludeObjectIDLocator: true,
});
});
});

View File

@@ -651,11 +651,15 @@ export const buildMongoFindCommand = (params: {
limit?: number;
skip?: number;
projection?: Record<string, unknown>;
includeObjectIDLocator?: boolean;
}): string => {
const command: Record<string, unknown> = {
find: String(params.collection || '').trim(),
filter: params.filter || {},
};
if (params.includeObjectIDLocator) {
command.__gonaviIncludeObjectIDLocator = true;
}
if (params.projection && Object.keys(params.projection).length > 0) {
command.projection = params.projection;
}