import type { Context } from "hono"; export type RpcCode = | "OK" | "CREATED" | "VALIDATION" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL" | "NETWORK"; export type ResponseLayer = "db" | "api" | "front"; export type RpcEnvelope = { ok: boolean; code: RpcCode | string; layer: ResponseLayer; message: string; context?: Record | null; data?: T | null; errors?: Record | string[] | null; }; export type ApiResponse = RpcEnvelope & { status: number }; const GENERIC_MESSAGES = new Set([ "ok", "error", "operación exitosa", "operacion exitosa", "algo salió mal", "algo salio mal", "error interno", "error interno del servidor", ]); export function isGenericMessage(message: string): boolean { return GENERIC_MESSAGES.has(message.trim().toLowerCase()); } export function mapRpcToStatus(code: string): number { switch (code) { case "OK": return 200; case "CREATED": return 201; case "VALIDATION": return 400; case "UNAUTHORIZED": return 401; case "FORBIDDEN": return 403; case "NOT_FOUND": return 404; case "CONFLICT": return 409; case "NETWORK": return 503; case "INTERNAL": default: return 500; } } export function respondRpc(c: Context, envelope: RpcEnvelope) { const status = mapRpcToStatus(String(envelope.code)); const body: ApiResponse = { ...envelope, status }; return c.json(body, status as 200 | 201 | 400 | 401 | 403 | 404 | 409 | 500 | 503); } export function respondApiError( c: Context, code: RpcCode, message: string, context?: Record, errors?: Record | string[] | null, ) { if (isGenericMessage(message)) { throw new Error(`API error message is too generic: ${message}`); } const status = mapRpcToStatus(code); const body: ApiResponse = { ok: false, code, status, layer: "api", message, context: context ?? {}, data: null, errors: errors ?? null, }; return c.json(body, status as 400 | 401 | 403 | 404 | 409 | 500 | 503); } export function enrichInfraError( fn: string, route: string, cause: string, detail?: string, ): { message: string; context: Record } { const message = detail ? `Falló la llamada a ${fn} en ${route}: ${cause} — ${detail}` : `Falló la llamada a ${fn} en ${route}: ${cause}`; return { message, context: { fn, route, cause }, }; } export function routeLabel(c: Context): string { return `${c.req.method} ${c.req.path}`; } export function onAppError(err: unknown, c: Context) { console.error(err); if (err && typeof err === "object" && (err as RpcEnvelope).layer) { const env = err as RpcEnvelope; return respondRpc(c, env); } const message = err instanceof Error ? `Error no controlado en ${routeLabel(c)}: ${err.message}` : `Error no controlado en ${routeLabel(c)}`; const status = 500; return c.json({ ok: false, code: "INTERNAL", status, layer: "api", message, context: { route: routeLabel(c) }, data: null, errors: null, }, status); }