85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
export interface PreviewOptions {
|
|
maxDepth?: number;
|
|
maxArrayItems?: number;
|
|
maxObjectKeys?: number;
|
|
maxStringLength?: number;
|
|
}
|
|
|
|
const DEFAULT_PREVIEW_OPTIONS: Required<PreviewOptions> = {
|
|
maxDepth: 4,
|
|
maxArrayItems: 6,
|
|
maxObjectKeys: 32,
|
|
maxStringLength: 1000,
|
|
};
|
|
|
|
function optionsWithDefaults(options: PreviewOptions = {}): Required<PreviewOptions> {
|
|
return {
|
|
maxDepth: options.maxDepth ?? DEFAULT_PREVIEW_OPTIONS.maxDepth,
|
|
maxArrayItems: options.maxArrayItems ?? DEFAULT_PREVIEW_OPTIONS.maxArrayItems,
|
|
maxObjectKeys: options.maxObjectKeys ?? DEFAULT_PREVIEW_OPTIONS.maxObjectKeys,
|
|
maxStringLength: options.maxStringLength ?? DEFAULT_PREVIEW_OPTIONS.maxStringLength,
|
|
};
|
|
}
|
|
|
|
export function jsonByteLength(value: unknown): number {
|
|
try {
|
|
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
} catch {
|
|
return Buffer.byteLength(String(value), "utf8");
|
|
}
|
|
}
|
|
|
|
export function previewJson(value: unknown, options: PreviewOptions = {}, depth = 0): unknown {
|
|
const limits = optionsWithDefaults(options);
|
|
if (value === null || typeof value === "number" || typeof value === "boolean") return value;
|
|
if (typeof value === "string") {
|
|
if (value.length <= limits.maxStringLength) return value;
|
|
return {
|
|
_previewType: "string",
|
|
length: value.length,
|
|
text: value.slice(0, limits.maxStringLength),
|
|
truncated: true,
|
|
};
|
|
}
|
|
if (typeof value !== "object") return String(value);
|
|
if (depth >= limits.maxDepth) {
|
|
return {
|
|
_previewType: Array.isArray(value) ? "array" : "object",
|
|
truncated: true,
|
|
...(Array.isArray(value) ? { length: value.length } : { keys: Object.keys(value as Record<string, unknown>).length }),
|
|
};
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return {
|
|
_previewType: "array",
|
|
length: value.length,
|
|
items: value.slice(0, limits.maxArrayItems).map((item) => previewJson(item, limits, depth + 1)),
|
|
truncatedItems: Math.max(0, value.length - limits.maxArrayItems),
|
|
};
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
const entries = Object.entries(record);
|
|
const preview: Record<string, unknown> = {};
|
|
for (const [key, item] of entries.slice(0, limits.maxObjectKeys)) {
|
|
preview[key] = previewJson(item, limits, depth + 1);
|
|
}
|
|
if (entries.length > limits.maxObjectKeys) {
|
|
preview._preview = {
|
|
truncatedKeys: entries.length - limits.maxObjectKeys,
|
|
totalKeys: entries.length,
|
|
};
|
|
}
|
|
return preview;
|
|
}
|
|
|
|
export function boundedJsonDetail(value: unknown, maxBytes: number, options: PreviewOptions = {}): unknown {
|
|
const originalBytes = jsonByteLength(value);
|
|
if (originalBytes <= maxBytes) return value;
|
|
return {
|
|
_truncated: true,
|
|
originalBytes,
|
|
maxBytes,
|
|
preview: previewJson(value, options),
|
|
};
|
|
}
|