mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix: preserve large integer precision when formatting JSON in value editor (fixes #2765)
* fix: preserve large integer precision when formatting JSON in value editor (fixes #2765) * fix: add missing showDiagramDialog to useNavigationTargets in AiAssistant
This commit is contained in:
@@ -3,6 +3,7 @@ import { formatError } from "@/lib/backend/errorUtils";
|
||||
import { computed, ref } from "vue";
|
||||
import type { MqRawResponse, TopicInfo } from "@/types/mq";
|
||||
import { mqRawRequest } from "@/lib/backend/api";
|
||||
import { safeJsonFormat } from "@/lib/common/safeJsonFormat";
|
||||
|
||||
interface Props {
|
||||
connectionId: string;
|
||||
@@ -156,7 +157,7 @@ function formatJsonBody() {
|
||||
const text = bodyText.value.trim();
|
||||
if (!text) return;
|
||||
try {
|
||||
bodyText.value = JSON.stringify(JSON.parse(text), null, 2);
|
||||
bodyText.value = safeJsonFormat(text, 2);
|
||||
error.value = undefined;
|
||||
} catch (e: unknown) {
|
||||
error.value = `JSON Body 格式错误: ${formatError(e)}`;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle
|
||||
import { createRedisShikiJsonHighlighter, type RedisJsonHighlighter } from "@/lib/redis/redisJsonHighlighter";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { formatTtl } from "@/lib/common/ttlFormat";
|
||||
import { safeJsonFormat } from "@/lib/common/safeJsonFormat";
|
||||
import { canEditRedisMemberDetail, clampRedisMemberDetailSheetWidth, formatRedisMemberDetail, getRedisMemberSelectionKey } from "@/lib/redis/redisValuePresentation";
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -347,8 +348,7 @@ function handleStringInput() {
|
||||
|
||||
function formatJsonText(raw: string): string | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
return safeJsonFormat(raw, 2);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -356,8 +356,7 @@ function formatJsonText(raw: string): string | null {
|
||||
|
||||
function compressJsonText(raw: string): string | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return JSON.stringify(parsed);
|
||||
return safeJsonFormat(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { safeJsonFormat } from "../safeJsonFormat";
|
||||
|
||||
describe("safeJsonFormat", () => {
|
||||
it("preserves large integers exceeding MAX_SAFE_INTEGER", () => {
|
||||
const input = '{"id":87712409002717401}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "id": 87712409002717401\n}');
|
||||
});
|
||||
|
||||
it("preserves negative large integers exceeding MIN_SAFE_INTEGER", () => {
|
||||
const input = '{"value":-87712409002717401}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "value": -87712409002717401\n}');
|
||||
});
|
||||
|
||||
it("preserves multiple large integers in the same JSON", () => {
|
||||
const input = '{"a":87712409002717401,"b":9007199254740992}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "a": 87712409002717401,\n "b": 9007199254740992\n}');
|
||||
});
|
||||
|
||||
it("does not modify integers within MAX_SAFE_INTEGER", () => {
|
||||
const input = '{"id":12345}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "id": 12345\n}');
|
||||
});
|
||||
|
||||
it("does not modify integers exactly at MAX_SAFE_INTEGER", () => {
|
||||
const input = `{"id":${Number.MAX_SAFE_INTEGER}}`;
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe(`{\n "id": ${Number.MAX_SAFE_INTEGER}\n}`);
|
||||
});
|
||||
|
||||
it("preserves floating point numbers with large integer parts", () => {
|
||||
const input = '{"value":87712409002717401.5}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "value": 87712409002717401.5\n}');
|
||||
});
|
||||
|
||||
it("compacts JSON without indent", () => {
|
||||
const input = '{\n "id": 87712409002717401\n}';
|
||||
const result = safeJsonFormat(input);
|
||||
expect(result).toBe('{"id":87712409002717401}');
|
||||
});
|
||||
|
||||
it("preserves large integers in nested JSON", () => {
|
||||
const input = '{"data":{"id":87712409002717401}}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "data": {\n "id": 87712409002717401\n }\n}');
|
||||
});
|
||||
|
||||
it("preserves large integers in arrays", () => {
|
||||
const input = '{"ids":[87712409002717401,87712409002717402]}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "ids": [\n 87712409002717401,\n 87712409002717402\n ]\n}');
|
||||
});
|
||||
|
||||
it("handles string values that look like numbers", () => {
|
||||
const input = '{"id":"87712409002717401"}';
|
||||
const result = safeJsonFormat(input, 2);
|
||||
expect(result).toBe('{\n "id": "87712409002717401"\n}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Parse and re-stringify JSON while preserving integer values that exceed
|
||||
* Number.MAX_SAFE_INTEGER (2^53 - 1). JavaScript's JSON.parse loses
|
||||
* precision on such values (e.g. 87712409002717401 → 87712409002717400),
|
||||
* so we replace large integer literals with string placeholders before
|
||||
* parsing and restore them after stringifying.
|
||||
*/
|
||||
export function safeJsonFormat(text: string, indent?: number): string {
|
||||
const MAX_SAFE_INT = String(Number.MAX_SAFE_INTEGER);
|
||||
const MIN_SAFE_INT = String(-Number.MAX_SAFE_INTEGER);
|
||||
const largeInts = new Map<string, string>();
|
||||
const placeholderPrefix = '"__DBX_BIGINT_';
|
||||
|
||||
// Replace large numeric literals (integers or floats with large integer parts)
|
||||
// with string placeholders to avoid precision loss.
|
||||
// Matches: optional minus, 16+ digits, optional decimal part, followed by
|
||||
// a JSON delimiter (comma, ], }, or end-of-string after whitespace).
|
||||
const replaced = text.replace(/(-?\d{16,}(?:\.\d+)?)(\s*(?:,|\]|}|$))/g, (_match, digits: string, tail: string) => {
|
||||
// Extract the integer part (before any decimal point) for comparison
|
||||
const integerPart = digits.split(".")[0];
|
||||
const isNegative = integerPart.startsWith("-");
|
||||
const absInteger = isNegative ? integerPart.slice(1) : integerPart;
|
||||
const threshold = isNegative ? MIN_SAFE_INT.slice(1) : MAX_SAFE_INT;
|
||||
if (absInteger.length < threshold.length) return _match;
|
||||
if (absInteger.length === threshold.length && absInteger <= threshold) return _match;
|
||||
|
||||
const key = String(largeInts.size);
|
||||
largeInts.set(key, digits);
|
||||
return `${placeholderPrefix}${key}"${tail}`;
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(replaced);
|
||||
let result = JSON.stringify(parsed, null, indent ?? undefined);
|
||||
|
||||
for (const [key, digits] of largeInts) {
|
||||
result = result.replace(`${placeholderPrefix}${key}"`, digits);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { safeJsonFormat } from "@/lib/common/safeJsonFormat";
|
||||
|
||||
export type CellDetailTab = "details" | "hexViewer" | "valueEditor";
|
||||
export type ValueEditorAction = "formatJson" | "setNull" | "restoreOriginal";
|
||||
|
||||
@@ -90,7 +92,7 @@ export function formatJsonText(text: string): string | undefined {
|
||||
if (!trimmed) return undefined;
|
||||
if (trimmed.length > CELL_DETAIL_JSON_FORMAT_MAX_LENGTH) return undefined;
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
||||
return safeJsonFormat(trimmed, 2);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -101,7 +103,7 @@ export function compactJsonText(text: string): string | undefined {
|
||||
if (!trimmed) return undefined;
|
||||
if (trimmed.length > CELL_DETAIL_JSON_FORMAT_MAX_LENGTH) return undefined;
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(trimmed));
|
||||
return safeJsonFormat(trimmed);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { KvKeyMetadata } from "@/lib/backend/api";
|
||||
|
||||
import { safeJsonFormat } from "@/lib/common/safeJsonFormat";
|
||||
|
||||
export interface PrettyPrintJsonResult {
|
||||
ok: boolean;
|
||||
value?: string;
|
||||
@@ -20,7 +22,7 @@ export interface ZooKeeperSummaryLabels {
|
||||
|
||||
export function prettyPrintJsonText(text: string): PrettyPrintJsonResult {
|
||||
try {
|
||||
return { ok: true, value: JSON.stringify(JSON.parse(text), null, 2) };
|
||||
return { ok: true, value: safeJsonFormat(text, 2) };
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_json" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user