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:
gggaiitx
2026-07-07 16:48:00 +08:00
committed by GitHub
parent 326c4f5495
commit 9410bf42a9
6 changed files with 116 additions and 8 deletions
@@ -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;
}
+3 -1
View File
@@ -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" };
}