mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(mongodb): display array structures as shell-style bson literals
This commit is contained in:
@@ -716,7 +716,9 @@ async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExporte
|
||||
}
|
||||
}
|
||||
|
||||
const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null);
|
||||
// Exports consume source text (CSV/SQL), not the live grid's BSON-faithful
|
||||
// cell encoding, so every document store keeps plain JSON cells here.
|
||||
const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null, { documentGridValues: false });
|
||||
if (result.columns.length === 0) result.columns = gridResult.value.columns;
|
||||
result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : kind === "elasticsearch" ? elasticsearchGridColumnTypesFor(result.columns) : kind === "solr" ? solrGridColumnTypesFor(result.columns) : undefined;
|
||||
result.affected_rows = exportedDocuments.length;
|
||||
|
||||
@@ -108,7 +108,7 @@ import type { BuildSingleColumnAlterSqlOptions } from "@/lib/table/tableStructur
|
||||
import { buildTableSelectSql, qualifyTableReferencesInSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { generateCellValues, type CellValueGenerationKind } from "@/lib/dataGrid/cellValueGeneration";
|
||||
import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentGridClipboardText, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue } from "@/lib/mongo/mongoDocumentValues";
|
||||
import { MONGO_DOCUMENT_GRID_NULL, mongoDocumentGridClipboardText, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridExternalValue, mongoDocumentGridInputValue, mongoDocumentRelaxedExtendedJson } from "@/lib/mongo/mongoDocumentValues";
|
||||
import { compactHeaderColumnType, formatMetadataColumnTypeLabel, isNumericColumnType, resolveDataGridTypeVisualKind, resolveHeaderColumnType, resolveResultColumnType } from "@/lib/dataGrid/dataGridColumnType";
|
||||
import { dataGridCellTextClass, dataGridTypeVisualClass } from "@/lib/dataGrid/dataGridCellTextVisual";
|
||||
import { DATA_GRID_TYPE_COLOR_KEYS, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
|
||||
@@ -745,6 +745,10 @@ const columnTypeMap = computed(() => {
|
||||
});
|
||||
const resolvedConnectionConfig = computed(() => connectionStore.getConfig(props.connectionId ?? ""));
|
||||
const resolvedDatabaseType = computed(() => props.databaseType ?? effectiveDatabaseTypeForConnection(resolvedConnectionConfig.value));
|
||||
// The collection grid and Mongo query-result grids share the document-grid value
|
||||
// encoding (BSON null sentinel + JSON-prefixed containers), so every display,
|
||||
// editor and clipboard path must decode it whenever MongoDB values are on screen.
|
||||
const usesMongoDocumentGridValues = computed(() => props.mongoCollectionGrid === true || resolvedDatabaseType.value === "mongodb");
|
||||
const isResultsContext = computed(() => props.context === "results");
|
||||
const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsContext.value && resolvedDatabaseType.value !== "victoriametrics");
|
||||
const canUseWhereSearch = computed(() => !!props.tableMeta && canShowWhereSearch.value);
|
||||
@@ -2210,7 +2214,7 @@ const columnAligns = computed<("left" | "right")[]>(() => {
|
||||
|
||||
function gridCellTextColorClass(item: RowItem, actualColIdx: number, visibleColIdx: number): string {
|
||||
const value = item.data[actualColIdx];
|
||||
const isGridNull = value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL);
|
||||
const isGridNull = value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL);
|
||||
if (isGridNull) return "text-muted-foreground italic";
|
||||
if (!colorizeDataGridCellTypes.value) return "text-foreground";
|
||||
const checkbox = booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && value !== null;
|
||||
@@ -2233,7 +2237,7 @@ function transposeCellTextColorClass(recordIndex: number, actualColIdx: number):
|
||||
const item = displayItems.value[recordIndex];
|
||||
if (!item) return "text-foreground";
|
||||
const value = item.data[actualColIdx];
|
||||
const isGridNull = value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL);
|
||||
const isGridNull = value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL);
|
||||
if (isGridNull) return "text-muted-foreground italic";
|
||||
if (!colorizeDataGridCellTypes.value) return "text-foreground";
|
||||
return dataGridCellTextClass({
|
||||
@@ -4019,7 +4023,7 @@ const editor = useDataGridEditor({
|
||||
includeDatabaseNameInSaveSql: computed(() => settingsStore.editorSettings.generateSqlIncludeDatabaseName),
|
||||
initialEditColumn: firstVisibleColumnIndex,
|
||||
cellEditorText: cellEditorTextForValue,
|
||||
normalizeEditorInput: (value) => (props.mongoCollectionGrid ? mongoDocumentGridInputValue(value) : value),
|
||||
normalizeEditorInput: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridInputValue(value) : value),
|
||||
getRowItem,
|
||||
pageSize,
|
||||
currentPage,
|
||||
@@ -4303,7 +4307,7 @@ function cellEditContentNeedsExpandedEditor(options: { displayText: string; edit
|
||||
}
|
||||
|
||||
function cellEditorTextForValue(value: CellValue | undefined, columnIndex: number): string {
|
||||
if (props.mongoCollectionGrid) {
|
||||
if (usesMongoDocumentGridValues.value) {
|
||||
const documentGridText = mongoDocumentGridEditorText(value);
|
||||
if (documentGridText !== undefined) return documentGridText;
|
||||
}
|
||||
@@ -4374,7 +4378,7 @@ function isIoTDBTimestampColumn(columnIndex: number): boolean {
|
||||
}
|
||||
|
||||
function inlineCellEditorText(value: CellValue, columnIndex: number): string {
|
||||
if (props.mongoCollectionGrid) {
|
||||
if (usesMongoDocumentGridValues.value) {
|
||||
const documentGridText = mongoDocumentGridEditorText(value);
|
||||
if (documentGridText !== undefined) return documentGridText;
|
||||
}
|
||||
@@ -5981,11 +5985,11 @@ const contextCellDetail = computed(() => {
|
||||
// The MongoDB collection grid stores an internal sentinel for explicit BSON
|
||||
// null; detail panes must render display text instead of leaking that marker.
|
||||
function gridDetailRawValue(value: CellValue): string {
|
||||
return props.mongoCollectionGrid ? (mongoDocumentGridDisplayText(value) ?? displayCellValue(value)) : displayCellValue(value);
|
||||
return usesMongoDocumentGridValues.value ? (mongoDocumentGridDisplayText(value) ?? displayCellValue(value)) : displayCellValue(value);
|
||||
}
|
||||
|
||||
function gridDetailIsNullValue(value: CellValue): boolean {
|
||||
return value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL);
|
||||
return value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL);
|
||||
}
|
||||
|
||||
function cellDetailFor(rowIndex: number, columnIndex: number): DataGridCellDetail | null {
|
||||
@@ -6030,7 +6034,9 @@ const mongoJsonPreviewFullText = computed(() => {
|
||||
const document = activeMongoJsonDocument.value;
|
||||
if (document === undefined) return "";
|
||||
try {
|
||||
return JSON.stringify(document, null, 2) ?? "";
|
||||
// Relaxed Extended JSON (`{"$date": "…"}`) like Compass's JSON view, instead
|
||||
// of the browser form's escaped `ISODate("…")` strings.
|
||||
return JSON.stringify(mongoDocumentRelaxedExtendedJson(document), null, 2) ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
@@ -6324,7 +6330,7 @@ const detailEdit = useDataGridCellDetailEdit({
|
||||
resultRows: computed(() => props.result.rows),
|
||||
getColumnInfo: (columnIndex) => tableColumnForGridColumn(columnIndex) ?? resultColumnInfoForGridColumn(columnIndex),
|
||||
cellEditorText: cellEditorTextForValue,
|
||||
normalizeEditorInput: (value) => (props.mongoCollectionGrid ? mongoDocumentGridInputValue(value) : value),
|
||||
normalizeEditorInput: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridInputValue(value) : value),
|
||||
nullValue: () => (props.mongoCollectionGrid ? MONGO_DOCUMENT_GRID_NULL : null),
|
||||
getRowItem,
|
||||
hydrateLargeValueCell,
|
||||
@@ -6656,7 +6662,7 @@ function primitiveCellFormatKey(value: CellValue, columnIndex?: number): string
|
||||
|
||||
function formatCell(value: CellValue, columnIndex?: number, originalBytes?: number, limitDisplay = true): string {
|
||||
const formatter = columnIndex === undefined ? undefined : resolvedColumnFormatters.value[columnIndex];
|
||||
if (props.mongoCollectionGrid) {
|
||||
if (usesMongoDocumentGridValues.value) {
|
||||
const documentGridText = mongoDocumentGridDisplayText(value, formatter);
|
||||
if (documentGridText !== undefined) return documentGridText;
|
||||
}
|
||||
@@ -7603,7 +7609,7 @@ function drawCanvasGrid() {
|
||||
searchMatchKeys: searchMatchSet.value,
|
||||
currentSearchMatch: currentSearchMatch.value,
|
||||
formatCell: (value, columnIndex, row) => formatCellCached(visibleLargeValuePreviewValue(row, columnIndex, value), columnIndex, largeValueOriginalBytes(row, columnIndex)),
|
||||
isNullValue: (value) => value === null || (props.mongoCollectionGrid === true && value === MONGO_DOCUMENT_GRID_NULL),
|
||||
isNullValue: (value) => value === null || (usesMongoDocumentGridValues.value && value === MONGO_DOCUMENT_GRID_NULL),
|
||||
newRowCellPlaceholder,
|
||||
isRowActive,
|
||||
rowCellsUseSelectionVisual,
|
||||
@@ -7935,8 +7941,8 @@ const {
|
||||
columnComments: visibleColumnComments,
|
||||
allColumnComments,
|
||||
displayValue: formatCellCached,
|
||||
cellClipboardText: (value) => (props.mongoCollectionGrid ? mongoDocumentGridClipboardText(value) : undefined),
|
||||
externalCellValue: (value) => (props.mongoCollectionGrid ? mongoDocumentGridExternalValue(value) : value),
|
||||
cellClipboardText: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridClipboardText(value) : undefined),
|
||||
externalCellValue: (value) => (usesMongoDocumentGridValues.value ? mongoDocumentGridExternalValue(value) : value),
|
||||
mongoDocuments: computed(() => props.result.mongo_copy_documents ?? props.result.mongo_documents),
|
||||
spatialColumns: computed(() => props.result.spatial_columns),
|
||||
spatialValues: computed(() => props.result.spatial_values),
|
||||
@@ -9588,7 +9594,7 @@ async function onGridKeydown(event: KeyboardEvent) {
|
||||
}
|
||||
|
||||
function detailClipboardText(detail: DataGridCellDetail): string {
|
||||
if (props.mongoCollectionGrid) {
|
||||
if (usesMongoDocumentGridValues.value) {
|
||||
const documentGridText = mongoDocumentGridClipboardText(detail.value);
|
||||
if (documentGridText !== undefined) return documentGridText;
|
||||
}
|
||||
@@ -9603,7 +9609,7 @@ function detailClipboardText(detail: DataGridCellDetail): string {
|
||||
// grid's BSON null marker is restored to a real null instead of leaking the
|
||||
// internal sentinel into clipboard JSON/TSV.
|
||||
function gridDetailExternalValue(value: CellValue): CellValue {
|
||||
return props.mongoCollectionGrid ? mongoDocumentGridExternalValue(value) : value;
|
||||
return usesMongoDocumentGridValues.value ? mongoDocumentGridExternalValue(value) : value;
|
||||
}
|
||||
|
||||
async function copyDetailValue() {
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("MongoDB document date display", () => {
|
||||
|
||||
it("formats nested dates without changing non-date BSON values", () => {
|
||||
const value = mongoDocumentGridValue({ event: { at: { $date: "2026-09-24T02:00:00.000Z" }, sequence: { $numberLong: "9007199254740993" } }, tags: ["a"] });
|
||||
expect(mongoDocumentGridDisplayText(value, dateFormatter("Asia/Shanghai"))).toBe('{"event":{"at":"2026-09-24 10:00:00","sequence":{"$numberLong":"9007199254740993"}},"tags":["a"]}');
|
||||
expect(mongoDocumentGridDisplayText(value, dateFormatter("Asia/Shanghai"))).toBe('{"event": {"at": "2026-09-24 10:00:00", "sequence": NumberLong("9007199254740993")}, "tags": ["a"]}');
|
||||
});
|
||||
|
||||
it("uses local time when no timezone override is configured", () => {
|
||||
@@ -32,9 +32,9 @@ describe("MongoDB document date display", () => {
|
||||
expect(mongoDocumentGridDisplayText(value, dateFormatter(undefined))).toMatch(/^2026-09-2[34] \d{2}:00:00$/);
|
||||
});
|
||||
|
||||
it("falls back to the original Extended JSON for an invalid timezone", () => {
|
||||
it("falls back to the shell-style display for an invalid timezone", () => {
|
||||
const value = mongoDocumentGridValue({ nested: { $date: "2026-09-24T02:00:00.000Z" } });
|
||||
expect(mongoDocumentGridDisplayText(value, dateFormatter("Invalid/Timezone"))).toBe('{"nested":{"$date":"2026-09-24T02:00:00.000Z"}}');
|
||||
expect(mongoDocumentGridDisplayText(value, dateFormatter("Invalid/Timezone"))).toBe('{"nested": ISODate("2026-09-24T02:00:00.000Z")}');
|
||||
});
|
||||
|
||||
it("keeps edit, copy, and external values independent from display formatting", () => {
|
||||
@@ -52,3 +52,38 @@ describe("MongoDB document date display", () => {
|
||||
expect(mongoDocumentGridDisplayText(value, dateFormatter("Asia/Shanghai"))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MongoDB array structure display", () => {
|
||||
it("renders arrays with shell-style BSON literals instead of the internal JSON encoding", () => {
|
||||
const value = mongoDocumentGridValue({
|
||||
ids: [{ $oid: "6743e4bfa3f6f84bc3fff6c8" }],
|
||||
history: [{ at: 'ISODate("2026-06-10T13:59:31.287Z")', by: { $oid: "507f1f77bcf86cd799439011" } }],
|
||||
counts: [{ $numberLong: "2326645729978441729" }, 2],
|
||||
});
|
||||
expect(mongoDocumentGridDisplayText(value)).toBe('{"ids": [ObjectId("6743e4bfa3f6f84bc3fff6c8")], "history": [{"at": ISODate("2026-06-10T13:59:31.287Z"), "by": ObjectId("507f1f77bcf86cd799439011")}], "counts": [NumberLong("2326645729978441729"), 2]}');
|
||||
});
|
||||
|
||||
it("keeps the structure separators readable for scalar arrays and nested containers", () => {
|
||||
expect(mongoDocumentGridDisplayText(mongoDocumentGridValue(["a", "b"]))).toBe('["a", "b"]');
|
||||
expect(mongoDocumentGridDisplayText(mongoDocumentGridValue({ tags: [], matrix: [[1, 2], [3]] }))).toBe('{"tags": [], "matrix": [[1, 2], [3]]}');
|
||||
});
|
||||
|
||||
it("still treats JSON-shaped BSON strings as data, not as structure", () => {
|
||||
expect(mongoDocumentGridDisplayText(mongoDocumentGridValue({ note: "[1,2]" }))).toBe('{"note": "[1,2]"}');
|
||||
});
|
||||
|
||||
it("keeps edit, copy, and external JSON untouched by the shell-style display", () => {
|
||||
const original = { items: [{ at: { $date: "2026-06-10T13:59:31.287Z" } }] };
|
||||
const value = mongoDocumentGridValue(original) as string;
|
||||
const rawJson = JSON.stringify(original);
|
||||
expect(mongoDocumentGridDisplayText(value)).toBe('{"items": [{"at": ISODate("2026-06-10T13:59:31.287Z")}]}');
|
||||
expect(mongoDocumentGridEditorText(value)).toBe(rawJson);
|
||||
expect(mongoDocumentGridClipboardText(value)).toBe(rawJson);
|
||||
expect(mongoDocumentGridExternalValue(value)).toBe(rawJson);
|
||||
});
|
||||
|
||||
it("renders out-of-range canonical dates as the raw wrapper instead of throwing", () => {
|
||||
const value = mongoDocumentGridValue({ d: { $date: { $numberLong: "9223372036854775807" } } });
|
||||
expect(mongoDocumentGridDisplayText(value)).toBe('{"d": {"$date": NumberLong("9223372036854775807")}}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,6 +147,93 @@ export function mongoDocumentDisplayValue(value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strings that are already shell-style BSON display literals. The driver ships
|
||||
* dates as `ISODate("…")` text and the grid renders other typed scalars the same
|
||||
* way, so inside arrays and subdocuments they must stay verbatim instead of
|
||||
* being re-quoted (which produced the escaped `ISODate(\"…\")` mess).
|
||||
*/
|
||||
const MONGO_SHELL_DISPLAY_LITERAL_PATTERN = /^(?:ISODate|new Date|ObjectId|NumberLong|NumberInt|NumberDouble|NumberDecimal)\(\s*(["']).+\1\s*\)$/;
|
||||
|
||||
/**
|
||||
* Renders a browser/extended-JSON BSON value the way mongosh prints it, so an
|
||||
* array cell reads `[ObjectId("…"), ISODate("…")]` instead of the internal
|
||||
* `[{"$oid":"…"},"ISODate(\\\"…\\\")"]` encoding. Display-only: edit, copy and
|
||||
* save paths keep the original JSON representation untouched.
|
||||
*/
|
||||
export function mongoDocumentDisplayText(value: unknown): string {
|
||||
if (value === null || value === undefined) return "null";
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "string") return MONGO_SHELL_DISPLAY_LITERAL_PATTERN.test(value) ? value : JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map((item) => mongoDocumentDisplayText(item)).join(", ")}]`;
|
||||
if (typeof value === "object") {
|
||||
const object = value as Record<string, unknown>;
|
||||
const wrapper = mongoShellWrapperDisplayText(object);
|
||||
if (wrapper !== undefined) return wrapper;
|
||||
return `{${Object.entries(object)
|
||||
.map(([key, item]) => `${JSON.stringify(key)}: ${mongoDocumentDisplayText(item)}`)
|
||||
.join(", ")}}`;
|
||||
}
|
||||
return JSON.stringify(String(value));
|
||||
}
|
||||
|
||||
function mongoShellWrapperDisplayText(object: Record<string, unknown>): string | undefined {
|
||||
const keys = Object.keys(object);
|
||||
if (keys.length !== 1) return undefined;
|
||||
const inner = object[keys[0] ?? ""];
|
||||
switch (keys[0]) {
|
||||
case "$oid":
|
||||
return typeof inner === "string" ? `ObjectId(${JSON.stringify(inner)})` : undefined;
|
||||
case "$numberLong":
|
||||
return typeof inner === "string" ? `NumberLong(${JSON.stringify(inner)})` : undefined;
|
||||
case "$numberInt":
|
||||
return typeof inner === "string" ? `NumberInt(${JSON.stringify(inner)})` : undefined;
|
||||
case "$numberDouble":
|
||||
return typeof inner === "string" ? `NumberDouble(${JSON.stringify(inner)})` : undefined;
|
||||
case "$numberDecimal":
|
||||
return typeof inner === "string" ? `NumberDecimal(${JSON.stringify(inner)})` : undefined;
|
||||
case "$date":
|
||||
return mongoShellDateWrapperDisplayText(inner);
|
||||
case "$minKey":
|
||||
return "MinKey()";
|
||||
case "$maxKey":
|
||||
return "MaxKey()";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function mongoShellDateWrapperDisplayText(inner: unknown): string | undefined {
|
||||
if (typeof inner === "string") return `ISODate(${JSON.stringify(inner)})`;
|
||||
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
|
||||
const long = (inner as Record<string, unknown>).$numberLong;
|
||||
if (typeof long === "string" && /^-?\d+$/.test(long)) {
|
||||
const millis = Number(long);
|
||||
// Dates beyond the JS Date range cannot round-trip through toISOString();
|
||||
// fall back to the raw wrapper instead of throwing past the grid formatter.
|
||||
if (Number.isFinite(millis) && Math.abs(millis) <= 8.64e15) {
|
||||
return `ISODate(${JSON.stringify(new Date(millis).toISOString())})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relaxed Extended JSON for read-only document previews: the browser form keeps
|
||||
* dates as `ISODate("…")` strings, and the preview must show them as
|
||||
* `{"$date": "…"}` like MongoDB Compass instead of escaped shell text.
|
||||
*/
|
||||
export function mongoDocumentRelaxedExtendedJson(value: unknown): unknown {
|
||||
const date = mongoShellDateToExtendedJson(value);
|
||||
if (date !== value) return date;
|
||||
if (Array.isArray(value)) return value.map((item) => mongoDocumentRelaxedExtendedJson(item));
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([key, item]) => [key, mongoDocumentRelaxedExtendedJson(item)]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps BSON values into the flat collection-grid representation. The grid
|
||||
* needs an internal sentinel for an explicit BSON null because an empty cell
|
||||
@@ -195,14 +282,19 @@ export function mongoDocumentGridDisplayText(value: unknown, formatter?: ColumnF
|
||||
}
|
||||
|
||||
function formatMongoDocumentGridJson(json: string, formatter: ColumnFormatterConfig | undefined): string {
|
||||
if (formatter?.kind !== "datetime" || !validMongoDisplayTimeZone(formatter.timezone)) return json;
|
||||
let value: unknown;
|
||||
try {
|
||||
const transformed = formatMongoDocumentDates(JSON.parse(json), formatter);
|
||||
if (!transformed.changed) return json;
|
||||
return typeof transformed.value === "string" ? transformed.value : JSON.stringify(transformed.value);
|
||||
value = JSON.parse(json);
|
||||
} catch {
|
||||
return json;
|
||||
}
|
||||
if (formatter?.kind === "datetime" && validMongoDisplayTimeZone(formatter.timezone)) {
|
||||
const transformed = formatMongoDocumentDates(value, formatter);
|
||||
if (transformed.changed) return typeof transformed.value === "string" ? transformed.value : mongoDocumentDisplayText(transformed.value);
|
||||
}
|
||||
// Shell-style structure text keeps BSON types inside arrays/subdocuments
|
||||
// readable (`[ObjectId("…"), ISODate("…")]`) instead of raw JSON wrappers.
|
||||
return mongoDocumentDisplayText(value);
|
||||
}
|
||||
|
||||
function validMongoDisplayTimeZone(timeZone: string | undefined): boolean {
|
||||
@@ -454,7 +546,8 @@ export function mongoDocumentIdForGrid(value: unknown): MongoInputValue {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function isMongoExtendedJsonId(value: unknown): value is Record<string, unknown> {
|
||||
/** A single typed scalar wrapper (`{$oid}` / `{$numberLong}`) the grid shows compactly. */
|
||||
export function isMongoExtendedJsonId(value: unknown): value is Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const object = value as Record<string, unknown>;
|
||||
const keys = Object.keys(object);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import { mongoDocumentIdForGrid } from "@/lib/mongo/mongoDocumentValues";
|
||||
import { isMongoExtendedJsonId, mongoDocumentGridValue, mongoDocumentIdForGrid } from "@/lib/mongo/mongoDocumentValues";
|
||||
import {
|
||||
chainedMethodCallPattern,
|
||||
describeMongoCommandParseFailure as describeMongoCommandParseFailureBasic,
|
||||
@@ -1136,7 +1136,17 @@ export function evaluateMongoAggregateSafety(command: MongoAggregateCommand, opt
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeMs: number, total: number, copyDocuments?: unknown[], totalIsExact = true): QueryResult {
|
||||
export interface MongoDocumentsToQueryResultOptions {
|
||||
/**
|
||||
* Encode arrays/subdocuments with the collection grid's BSON-faithful values so
|
||||
* display, editing and clipboard share one representation. Export paths that
|
||||
* need source text pass `false` and keep plain JSON cells.
|
||||
*/
|
||||
documentGridValues?: boolean;
|
||||
}
|
||||
|
||||
export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeMs: number, total: number, copyDocuments?: unknown[], totalIsExact = true, options: MongoDocumentsToQueryResultOptions = {}): QueryResult {
|
||||
const documentGridValues = options.documentGridValues !== false;
|
||||
const columns: string[] = [];
|
||||
|
||||
for (const doc of documents) {
|
||||
@@ -1150,8 +1160,8 @@ export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeM
|
||||
}
|
||||
|
||||
const rows = documents.map((doc) => {
|
||||
if (isRecord(doc)) return columns.map((column) => toCellValue(doc[column]));
|
||||
return columns.map((column) => (column === "value" ? toCellValue(doc) : null));
|
||||
if (isRecord(doc)) return columns.map((column) => toCellValue(doc[column], documentGridValues));
|
||||
return columns.map((column) => (column === "value" ? toCellValue(doc, documentGridValues) : null));
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -2030,8 +2040,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function toCellValue(value: unknown): string | number | boolean | null {
|
||||
function toCellValue(value: unknown, documentGridValues = false): string | number | boolean | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
||||
return mongoDocumentIdForGrid(value);
|
||||
// Typed scalar ids keep their compact grid display; arrays and subdocuments take the
|
||||
// document-grid encoding so display, editing and clipboard share the collection grid's
|
||||
// representation instead of dumping the internal JSON encoding into the cell.
|
||||
if (isMongoExtendedJsonId(value)) return mongoDocumentIdForGrid(value);
|
||||
return documentGridValues ? (mongoDocumentGridValue(value) as string) : mongoDocumentIdForGrid(value);
|
||||
}
|
||||
|
||||
@@ -9118,7 +9118,9 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (pageDocuments.length === 0 || pageDocuments.length < plan.requestLimit || reachedLogicalLimit || reachedExactTotal) break;
|
||||
}
|
||||
|
||||
const result = mongoDocumentsToQueryResult(documents, performance.now() - exportStartedAt, totalRows ?? documents.length, copyDocuments, totalRows !== null);
|
||||
// Full exports return source rows (CSV/SQL text), not the live grid's
|
||||
// BSON-faithful cell encoding.
|
||||
const result = mongoDocumentsToQueryResult(documents, performance.now() - exportStartedAt, totalRows ?? documents.length, copyDocuments, totalRows !== null, { documentGridValues: false });
|
||||
if (result.columns.length === 0) {
|
||||
result.columns = tab.result.columns;
|
||||
result.column_types = tab.result.column_types;
|
||||
|
||||
+24
@@ -10,6 +10,7 @@ import {
|
||||
formatMongoShellLiteral,
|
||||
MONGO_DOCUMENT_GRID_NULL,
|
||||
mongoDocumentDisplayValue,
|
||||
mongoDocumentDisplayText,
|
||||
mongoDocumentGridDisplayText,
|
||||
mongoDocumentGridClipboardText,
|
||||
mongoDocumentGridEditorText,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
mongoDocumentGridValue,
|
||||
mongoDocumentGridColumnTypes,
|
||||
mongoDocumentIdForGrid,
|
||||
mongoDocumentRelaxedExtendedJson,
|
||||
parseMongoDocumentInputValue,
|
||||
serializeMongoDocumentId,
|
||||
} from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts";
|
||||
@@ -344,6 +346,28 @@ test("keeps normal Mongo values readable and unsafe Int64 editable", () => {
|
||||
assert.deepEqual(parseMongoDocumentInputValue('NumberLong("9007199254740993")'), { $numberLong: "9007199254740993" });
|
||||
});
|
||||
|
||||
test("renders Mongo structures as shell-style literals like mongosh", () => {
|
||||
assert.equal(mongoDocumentDisplayText({ $oid: "6743e4bfa3f6f84bc3fff6c8" }), 'ObjectId("6743e4bfa3f6f84bc3fff6c8")');
|
||||
assert.equal(mongoDocumentDisplayText({ $numberLong: "2326645729978441729" }), 'NumberLong("2326645729978441729")');
|
||||
assert.equal(mongoDocumentDisplayText({ $numberInt: "42" }), 'NumberInt("42")');
|
||||
assert.equal(mongoDocumentDisplayText({ $date: "2026-06-10T13:59:31.287Z" }), 'ISODate("2026-06-10T13:59:31.287Z")');
|
||||
assert.equal(mongoDocumentDisplayText({ $date: { $numberLong: "1781099971287" } }), 'ISODate("2026-06-10T13:59:31.287Z")');
|
||||
assert.equal(mongoDocumentDisplayText({ $minKey: 1 }), "MinKey()");
|
||||
assert.equal(mongoDocumentDisplayText(['ISODate("2026-06-10T13:59:31.287Z")', { $oid: "507f1f77bcf86cd799439011" }, 2]), '[ISODate("2026-06-10T13:59:31.287Z"), ObjectId("507f1f77bcf86cd799439011"), 2]');
|
||||
assert.equal(mongoDocumentDisplayText({ note: "[1,2]", nested: [[1], []] }), '{"note": "[1,2]", "nested": [[1], []]}');
|
||||
assert.equal(mongoDocumentDisplayText({ $binary: { base64: "AQI=", subType: "00" } }), '{"$binary": {"base64": "AQI=", "subType": "00"}}');
|
||||
});
|
||||
|
||||
test("converts browser-form dates to relaxed Extended JSON for document previews", () => {
|
||||
assert.deepEqual(mongoDocumentRelaxedExtendedJson({ at: 'ISODate("2026-06-10T13:59:31.287Z")', keep: "ISODate-ish text" }), {
|
||||
at: { $date: "2026-06-10T13:59:31.287Z" },
|
||||
keep: "ISODate-ish text",
|
||||
});
|
||||
assert.deepEqual(mongoDocumentRelaxedExtendedJson({ items: [{ by: { $oid: "507f1f77bcf86cd799439011" }, at: 'new Date("2026-06-10T13:59:31.287Z")' }] }), {
|
||||
items: [{ by: { $oid: "507f1f77bcf86cd799439011" }, at: { $date: "2026-06-10T13:59:31.287Z" } }],
|
||||
});
|
||||
});
|
||||
|
||||
test("builds edits for Int32, Double, Date, and unsafe Int64", () => {
|
||||
const changes = new Map<number, string | number | boolean | null>([
|
||||
[1, 42],
|
||||
|
||||
+15
-2
@@ -38,7 +38,7 @@ import {
|
||||
splitMongoCommandRanges,
|
||||
} from "../../apps/desktop/src/lib/mongo/mongoShellCommand.ts";
|
||||
import type { MongoWriteCommand } from "../../apps/desktop/src/lib/mongo/mongoShellCommand.ts";
|
||||
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts";
|
||||
import { buildMongoUpdateDocument as buildMongoDocumentUpdate, formatMongoShellLiteral as formatMongoDocumentShellLiteral, mongoDocumentGridDisplayText, mongoDocumentGridEditorText, mongoDocumentGridValue } from "../../apps/desktop/src/lib/mongo/mongoDocumentValues.ts";
|
||||
import { normalizeJsonArgument } from "../mongo-shell/src/json.ts";
|
||||
|
||||
test("parseMongoFindCommand parses db collection find with an empty JSON filter", () => {
|
||||
@@ -1751,7 +1751,7 @@ test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => {
|
||||
|
||||
assert.deepEqual(result.columns, ["_id", "name", "profile", "active"]);
|
||||
assert.deepEqual(result.rows, [
|
||||
["1", "Ada", '{"role":"admin"}', null],
|
||||
["1", "Ada", mongoDocumentGridValue({ role: "admin" }), null],
|
||||
["2", "Lin", null, true],
|
||||
]);
|
||||
assert.deepEqual(result.mongo_documents, [
|
||||
@@ -1763,6 +1763,19 @@ test("mongoDocumentsToQueryResult turns mongo documents into grid rows", () => {
|
||||
assert.equal(result.truncated, true);
|
||||
});
|
||||
|
||||
test("mongoDocumentsToQueryResult displays array structures with shell-style BSON literals", () => {
|
||||
const document = {
|
||||
_id: "1",
|
||||
history: [{ at: 'ISODate("2026-06-10T13:59:31.287Z")', by: { $oid: "507f1f77bcf86cd799439011" } }],
|
||||
};
|
||||
const result = mongoDocumentsToQueryResult([document], 5, 1);
|
||||
const historyCell = result.rows[0]?.[1];
|
||||
|
||||
assert.equal(typeof historyCell, "string");
|
||||
assert.equal(mongoDocumentGridDisplayText(historyCell), '[{"at": ISODate("2026-06-10T13:59:31.287Z"), "by": ObjectId("507f1f77bcf86cd799439011")}]');
|
||||
assert.equal(mongoDocumentGridEditorText(historyCell), JSON.stringify(document.history));
|
||||
});
|
||||
|
||||
test("mongoDocumentsToQueryResult keeps aligned extended documents for copying", () => {
|
||||
const documents = [{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, createdAt: 'ISODate("2026-07-24T00:00:00Z")' }];
|
||||
const copyDocuments = [{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, createdAt: { $date: "2026-07-24T00:00:00Z" } }];
|
||||
|
||||
Reference in New Issue
Block a user