fix(export): preserve MongoDB Extended JSON
MongoDB JSON exports now use mongo_copy_documents so nested values and BSON date and object wrappers survive serialization without changing SQL export rows. Closes #8558
This commit is contained in:
2 files changed
+57
-8
No files matched your search
@@ -7,7 +7,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import type { DataGridTableMeta } from "@/lib/dataGrid/dataGridSql";
|
||||
import type { CellSelectionMatrix, SelectionData } from "@/lib/dataGrid/gridSelection";
|
||||
import type { CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import { extractDataGridSelection } from "@/lib/backend/api";
|
||||
import { extractDataGridSelection, exportQueryResultJson } from "@/lib/backend/api";
|
||||
import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS } from "@/lib/dataGrid/dataGridCopyExtractor";
|
||||
import { clearDataGridClipboardCopy, parseDataGridClipboard } from "@/lib/dataGrid/dataGridClipboard";
|
||||
|
||||
@@ -46,6 +46,7 @@ vi.mock("@/lib/backend/api", async (importOriginal) => {
|
||||
return {
|
||||
...original,
|
||||
extractDataGridSelection: vi.fn(),
|
||||
exportQueryResultJson: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -70,6 +71,7 @@ function createMongoExportState(options: {
|
||||
mongoUpdateTarget?: false;
|
||||
contextColumn?: number;
|
||||
syntheticContext?: boolean;
|
||||
fullExportResult?: UseDataGridExportOptions["fullExportResult"];
|
||||
}) {
|
||||
const items = options.items ?? [options.item];
|
||||
const selectedRowIds = options.selectedRowIds ?? new Set<number>();
|
||||
@@ -99,6 +101,7 @@ function createMongoExportState(options: {
|
||||
getRowItem: (rowId) => items.find((item) => item.id === rowId),
|
||||
selectedRowIds: ref(selectedRowIds),
|
||||
hasRowSelection: computed(() => selectedRowIds.size > 0),
|
||||
fullExportResult: options.fullExportResult,
|
||||
};
|
||||
return useDataGridExport(state);
|
||||
}
|
||||
@@ -1509,6 +1512,33 @@ describe("useDataGridExport prepared row statements", () => {
|
||||
expect(extractDataGridSelection).not.toHaveBeenCalled();
|
||||
expect(copyToClipboard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves Mongo Extended JSON objects and dates in JSON exports", async () => {
|
||||
const columns = ["_id", "valueMap", "createdTime"];
|
||||
const mongoCopyDocument = {
|
||||
_id: { $oid: "6a9fb51db2f0c46b94002f26" },
|
||||
valueMap: { field1: "10147E", field2: "0" },
|
||||
createdTime: { $date: "2026-09-08T07:11:25.458Z" },
|
||||
};
|
||||
const item = { ...row(["6a9fb51db2f0c46b94002f26", JSON.stringify(mongoCopyDocument.valueMap), 'ISODate("2026-09-08T07:11:25.458Z")']), sourceIndex: 0 };
|
||||
const state = createMongoExportState({
|
||||
columns,
|
||||
item,
|
||||
mongoDocuments: [mongoCopyDocument],
|
||||
fullExportResult: async () => ({
|
||||
columns,
|
||||
column_types: ["", "", "datetime"],
|
||||
rows: [item.data as [string, string, string]],
|
||||
mongo_copy_documents: [mongoCopyDocument],
|
||||
affected_rows: 1,
|
||||
execution_time_ms: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
await state.exportJson();
|
||||
|
||||
expect(exportQueryResultJson).toHaveBeenCalledWith(expect.any(String), columns, [[mongoCopyDocument._id, mongoCopyDocument.valueMap, mongoCopyDocument.createdTime]]);
|
||||
});
|
||||
});
|
||||
|
||||
// issue #7471:文本型 MySQL VARBINARY 复制单元格/多选/整行时,外部剪贴板应呈现原始字符串;
|
||||
|
||||
@@ -331,7 +331,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; columnComments: Array<string | undefined>; rows: CellValue[][] } {
|
||||
function normalizeCompleteLocalResult(result: QueryResult): { columns: string[]; columnTypes: string[]; columnComments: Array<string | undefined>; rows: CellValue[][]; mongoCopyDocuments?: unknown[] } {
|
||||
const hiddenColumnIndexes = new Set(result.hidden_column_indexes ?? []);
|
||||
const exportedColumnIndexes = result.columns.map((_, index) => index).filter((index) => !hiddenColumnIndexes.has(index));
|
||||
const hasHiddenColumns = exportedColumnIndexes.length !== result.columns.length;
|
||||
@@ -345,15 +345,34 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
columnTypes: hasHiddenColumns ? exportedColumnIndexes.map((index) => result.column_types?.[index] ?? "") : (result.column_types ?? []),
|
||||
columnComments: hasHiddenColumns ? exportedColumnIndexes.map((index) => allXlsxColumnComments.value[index]) : [...allXlsxColumnComments.value],
|
||||
rows: hasHiddenColumns ? rows.map((row) => exportedColumnIndexes.map((index) => row[index])) : rows,
|
||||
mongoCopyDocuments: result.mongo_copy_documents?.slice(0, rows.length),
|
||||
};
|
||||
}
|
||||
|
||||
function mongoDocumentRowsForJson(columnsToExport: string[], rows: CellValue[][], documents: unknown[] | undefined): CellValue[][] {
|
||||
if (databaseType.value !== "mongodb" || !documents || documents.length !== rows.length) return rows;
|
||||
return rows.map((row, rowIndex) => {
|
||||
const document = documents[rowIndex];
|
||||
if (!document || typeof document !== "object" || Array.isArray(document)) return row;
|
||||
const source = document as Record<string, unknown>;
|
||||
return columnsToExport.map((column, columnIndex) => (Object.prototype.hasOwnProperty.call(source, column) ? (source[column] as CellValue) : (row[columnIndex] ?? null)));
|
||||
});
|
||||
}
|
||||
|
||||
function mongoLocalRowsForJson(items: RowItem[]): CellValue[][] {
|
||||
return items.map((item) => {
|
||||
const document = rowToJsonObject(item);
|
||||
return columns.value.map((column) => (document[column] as CellValue) ?? null);
|
||||
});
|
||||
}
|
||||
|
||||
async function resultToExport(
|
||||
rowIds?: number[],
|
||||
onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void,
|
||||
useFullExport = true,
|
||||
formatDateTime = true,
|
||||
headerMode: XlsxHeaderMode = "name",
|
||||
preserveMongoExtendedJson = false,
|
||||
): Promise<{
|
||||
columns: string[];
|
||||
columnTypes: string[];
|
||||
@@ -367,7 +386,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
if (result) {
|
||||
const columnComments = buildXlsxHeaderOverrides(result.columns, commentsForExportColumns(result.columns), headerMode);
|
||||
return {
|
||||
...applyGlobalDateTimeExportFormat({ columns: result.columns, columnTypes: result.column_types ?? [], rows: result.rows }, formatDateTime),
|
||||
...applyGlobalDateTimeExportFormat({ columns: result.columns, columnTypes: result.column_types ?? [], rows: preserveMongoExtendedJson ? mongoDocumentRowsForJson(result.columns, result.rows, result.mongo_copy_documents) : result.rows }, formatDateTime && !preserveMongoExtendedJson),
|
||||
columnComments,
|
||||
spatialColumns: result.spatial_columns,
|
||||
spatialValues: result.spatial_values,
|
||||
@@ -383,7 +402,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
const normalized = normalizeCompleteLocalResult(completeLocalResult.value);
|
||||
const columnComments = buildXlsxHeaderOverrides(normalized.columns, normalized.columnComments, headerMode);
|
||||
return {
|
||||
...applyGlobalDateTimeExportFormat({ columns: normalized.columns, columnTypes: normalized.columnTypes, rows: normalized.rows }, formatDateTime),
|
||||
...applyGlobalDateTimeExportFormat({ columns: normalized.columns, columnTypes: normalized.columnTypes, rows: preserveMongoExtendedJson ? mongoDocumentRowsForJson(normalized.columns, normalized.rows, normalized.mongoCopyDocuments) : normalized.rows }, formatDateTime && !preserveMongoExtendedJson),
|
||||
columnComments,
|
||||
spatialColumns: completeLocalResult.value.spatial_columns,
|
||||
spatialValues: completeLocalResult.value.spatial_values,
|
||||
@@ -399,9 +418,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
{
|
||||
columns: columns.value,
|
||||
columnTypes: (columnTypes.value ?? []).map((type) => type ?? ""),
|
||||
rows: exportItems.map((item) => item.data),
|
||||
rows: preserveMongoExtendedJson && databaseType.value === "mongodb" ? mongoLocalRowsForJson(exportItems) : exportItems.map((item) => item.data),
|
||||
},
|
||||
formatDateTime,
|
||||
formatDateTime && !preserveMongoExtendedJson,
|
||||
),
|
||||
columnComments: commentHeader,
|
||||
...(spatialColumns?.length ? { spatialColumns } : {}),
|
||||
@@ -842,7 +861,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(rowIds);
|
||||
const result = await resultToExport(rowIds, undefined, true, true, "name", true);
|
||||
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
@@ -864,7 +883,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
if (!path) return;
|
||||
outputPath = path as string;
|
||||
}
|
||||
const result = await resultToExport(undefined, undefined, false);
|
||||
const result = await resultToExport(undefined, undefined, false, true, "name", true);
|
||||
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
|
||||
toast(t("grid.exported"));
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in new issue
Block a user