feat: add result formatting helpers
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
export const DEFAULT_ROW_LIMIT = 100
|
||||||
|
export const MAX_ROW_LIMIT = 1000
|
||||||
|
|
||||||
|
export const clampRowLimit = (requested?: number): number => {
|
||||||
|
if (requested === undefined) return DEFAULT_ROW_LIMIT
|
||||||
|
return Math.max(1, Math.min(Math.trunc(requested), MAX_ROW_LIMIT))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const truncateRows = <T>(rows: T[], limit: number): { rows: T[]; truncated: boolean } =>
|
||||||
|
rows.length > limit
|
||||||
|
? { rows: rows.slice(0, limit), truncated: true }
|
||||||
|
: { rows, truncated: false }
|
||||||
|
|
||||||
|
export const normalizeCell = (value: unknown): unknown => {
|
||||||
|
if (typeof value === 'bigint') return value.toString()
|
||||||
|
if (value instanceof Date) return value.toISOString()
|
||||||
|
if (Buffer.isBuffer(value)) return `\\x${value.toString('hex')}`
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
type DbErrorShape = Error & {
|
||||||
|
code?: string | number
|
||||||
|
detail?: string
|
||||||
|
hint?: string
|
||||||
|
sqlMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDbError = (engine: string, error: unknown): string => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const e = error as DbErrorShape
|
||||||
|
const code = e.code !== undefined && e.code !== null ? ` ${e.code}` : ''
|
||||||
|
const message = e.sqlMessage ?? e.message
|
||||||
|
const extras = [e.detail, e.hint].filter(Boolean).join(' | ')
|
||||||
|
return `[${engine}${code}] ${message}${extras ? ` (${extras})` : ''}`
|
||||||
|
}
|
||||||
|
return `[${engine}] ${String(error)}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
clampRowLimit,
|
||||||
|
DEFAULT_ROW_LIMIT,
|
||||||
|
formatDbError,
|
||||||
|
MAX_ROW_LIMIT,
|
||||||
|
normalizeCell,
|
||||||
|
truncateRows
|
||||||
|
} from '../../src/format.js'
|
||||||
|
|
||||||
|
describe('clampRowLimit', () => {
|
||||||
|
it('defaults when undefined', () => {
|
||||||
|
expect(clampRowLimit(undefined)).toBe(DEFAULT_ROW_LIMIT)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps to max and min and truncates fractions', () => {
|
||||||
|
expect(clampRowLimit(99999)).toBe(MAX_ROW_LIMIT)
|
||||||
|
expect(clampRowLimit(0)).toBe(1)
|
||||||
|
expect(clampRowLimit(-5)).toBe(1)
|
||||||
|
expect(clampRowLimit(10.9)).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('truncateRows', () => {
|
||||||
|
it('passes short arrays through', () => {
|
||||||
|
expect(truncateRows([1, 2], 5)).toEqual({ rows: [1, 2], truncated: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('slices long arrays and flags truncation', () => {
|
||||||
|
const { rows, truncated } = truncateRows([1, 2, 3], 2)
|
||||||
|
expect(rows).toEqual([1, 2])
|
||||||
|
expect(truncated).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('normalizeCell', () => {
|
||||||
|
it('converts bigint to string', () => {
|
||||||
|
expect(normalizeCell(123n)).toBe('123')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('converts Date to ISO string', () => {
|
||||||
|
expect(normalizeCell(new Date('2026-01-02T03:04:05Z'))).toBe('2026-01-02T03:04:05.000Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('converts Buffer to hex string', () => {
|
||||||
|
expect(normalizeCell(Buffer.from([0xde, 0xad]))).toBe('\\xdead')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes primitives and null through', () => {
|
||||||
|
expect(normalizeCell('x')).toBe('x')
|
||||||
|
expect(normalizeCell(5)).toBe(5)
|
||||||
|
expect(normalizeCell(null)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatDbError', () => {
|
||||||
|
it('formats code, detail and hint', () => {
|
||||||
|
const error = Object.assign(new Error('relation "x" does not exist'), {
|
||||||
|
code: '42P01',
|
||||||
|
hint: 'check the table name'
|
||||||
|
})
|
||||||
|
expect(formatDbError('postgres', error)).toBe(
|
||||||
|
'[postgres 42P01] relation "x" does not exist (check the table name)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefers sqlMessage for mysql errors', () => {
|
||||||
|
const error = Object.assign(new Error('outer'), {
|
||||||
|
code: 'ER_NO_SUCH_TABLE',
|
||||||
|
sqlMessage: "Table 'a.b' doesn't exist"
|
||||||
|
})
|
||||||
|
expect(formatDbError('mysql', error)).toBe(
|
||||||
|
"[mysql ER_NO_SUCH_TABLE] Table 'a.b' doesn't exist"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles non-Error values', () => {
|
||||||
|
expect(formatDbError('mysql', 'boom')).toBe('[mysql] boom')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user