mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 14:33:17 +00:00
- Replace db.prepare() in payroll_http with callCoreFn for attendance, loans, loan receipts, and legacy payroll period routes - Use respondRpc/respondApiError for standardized HTTP responses - Add fn_loan_list/get, fn_loan_payment_get, and fn_payroll_period_* RPCs to 013-rpc-payroll.sql with grants - Refactor excel storeDocument to fn_worker_document_store (S3 stays in TS) - Refactor importExcel and upsertWorker to fn_workers_import_batch Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
130 lines
3.2 KiB
TypeScript
130 lines
3.2 KiB
TypeScript
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<T = unknown> = {
|
|
ok: boolean;
|
|
code: RpcCode | string;
|
|
layer: ResponseLayer;
|
|
message: string;
|
|
context?: Record<string, unknown> | null;
|
|
data?: T | null;
|
|
errors?: Record<string, string> | string[] | null;
|
|
};
|
|
|
|
export type ApiResponse<T = unknown> = RpcEnvelope<T> & { 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<T>(c: Context, envelope: RpcEnvelope<T>) {
|
|
const status = mapRpcToStatus(String(envelope.code));
|
|
const body: ApiResponse<T> = { ...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<string, unknown>,
|
|
errors?: Record<string, string> | 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<string, unknown> } {
|
|
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);
|
|
}
|