BillTracker/middleware/errorFormatter.cts

90 lines
2.8 KiB
TypeScript
Raw Normal View History

2026-05-09 13:03:36 -05:00
/**
* Centralized Error Formatter Middleware
*
2026-05-09 13:03:36 -05:00
* Standard error response format:
* { "error", "message", "field", "code" }
2026-05-09 13:03:36 -05:00
*/
import type { Req, Res, Next } from '../types/http';
const { ValidationError, formatError } = require('../utils/apiError.cts');
2026-05-09 13:03:36 -05:00
/** Extract field name from various validation error patterns */
function extractFieldFromError(message: any): string | null {
2026-05-09 13:03:36 -05:00
if (!message) return null;
2026-05-09 13:03:36 -05:00
const fieldMatch = message.match(/^(\w+)\s+(?:must be|is|are)\s+/i);
if (fieldMatch) return fieldMatch[1];
const yearMonthMatch = message.match(
/^(year|month|due_day|interest_rate|actual_amount|start_year|start_month|end_year|end_month)\s+must\b/i,
);
2026-05-09 13:03:36 -05:00
if (yearMonthMatch) return yearMonthMatch[1];
const requiredMatch = message.match(
/^(username|password|name|bill_id|category_id|due_day)\s+(?:is|are)\s+required/i,
);
2026-05-09 13:03:36 -05:00
if (requiredMatch) return requiredMatch[1];
2026-05-09 13:03:36 -05:00
return null;
}
/** Convert a plain error message/string into a standardized error object */
function standardizeError(message: any, code = 'VALIDATION_ERROR', fieldOverride: any = null) {
2026-05-09 13:03:36 -05:00
const field = fieldOverride || extractFieldFromError(message);
2026-05-09 13:03:36 -05:00
return {
error: code,
message: typeof message === 'string' ? message : String(message),
field: field || null,
code: code,
2026-05-09 13:03:36 -05:00
};
}
/** Express middleware that ensures all error responses follow standard format */
function errorFormatter(req: Req, res: Res, next: Next): void {
2026-05-09 13:03:36 -05:00
const originalJson = res.json;
res.json = function (this: any, data: any) {
if (data && typeof data === 'object' && !Array.isArray(data) && data.error && !data.success) {
// Already standardized (machine-readable code + human message) — pass through
if (typeof data.code === 'string' && data.code && typeof data.message === 'string') {
return originalJson.call(this, data);
}
const message = typeof data.message === 'string' && data.message ? data.message : data.error;
const code = typeof data.code === 'string' && data.code ? data.code : 'ERROR';
const standardized = standardizeError(message, code, data.field);
2026-05-09 13:03:36 -05:00
return originalJson.call(this, standardized);
}
return originalJson.call(this, data);
};
2026-05-09 13:03:36 -05:00
next();
}
function formatValidationError(message: any, field?: any) {
2026-05-09 13:03:36 -05:00
return standardizeError(message, 'VALIDATION_ERROR', field);
}
function formatNotFoundError(message: any, field?: any) {
2026-05-09 13:03:36 -05:00
return standardizeError(message, 'NOT_FOUND', field);
}
function formatAuthError(message: any) {
2026-05-09 13:03:36 -05:00
return standardizeError(message, 'AUTH_ERROR');
}
function formatForbiddenError(message: any) {
2026-05-09 13:03:36 -05:00
return standardizeError(message, 'FORBIDDEN');
}
module.exports = {
errorFormatter,
standardizeError,
formatValidationError,
formatNotFoundError,
formatAuthError,
formatForbiddenError,
extractFieldFromError,
};