UNPKG

ultimate-crud

Version:

Ultimate dynamic CRUD API generator with REST, GraphQL, OpenAPI support and association handling for Node.js/Express/Sequelize

302 lines (269 loc) 9.07 kB
/** * Error handling utilities for Ultimate CRUD * Provides proper HTTP status codes based on database error types * * @license MIT * @copyright 2025 cnos-dev * @author Harish Kashyap (CNOS Dev) */ /** * Maps Sequelize error types to appropriate HTTP status codes * @param {Error} error - The error object from Sequelize * @returns {Object} - { status, message, details } */ function mapErrorToHttpStatus(error) { // Default error response const defaultError = { status: 500, message: "Internal server error", details: error.message }; if (!error) return defaultError; // Sequelize validation errors if (error.name === 'SequelizeValidationError') { return { status: 422, // Unprocessable Entity message: "Validation error", details: error.errors?.map(e => ({ field: e.path, message: e.message, value: e.value })) || error.message }; } // Unique constraint violations if (error.name === 'SequelizeUniqueConstraintError') { return { status: 409, // Conflict message: "Resource already exists", details: { fields: error.errors?.map(e => e.path) || [], message: error.errors?.[0]?.message || "Unique constraint violation", constraint: error.parent?.constraint || error.original?.constraint } }; } // Foreign key constraint violations if (error.name === 'SequelizeForeignKeyConstraintError') { return { status: 422, // Unprocessable Entity message: "Foreign key constraint violation", details: { field: error.fields?.[0] || error.index, message: "Referenced resource does not exist", constraint: error.parent?.constraint || error.original?.constraint } }; } // Not null constraint violations if (error.name === 'SequelizeNotNullConstraintError') { return { status: 422, // Unprocessable Entity message: "Missing required field", details: { field: error.path, message: `Field '${error.path}' cannot be null` } }; } // Database connection errors if (error.name === 'SequelizeConnectionError' || error.name === 'SequelizeConnectionRefusedError' || error.name === 'SequelizeHostNotFoundError') { return { status: 503, // Service Unavailable message: "Database connection error", details: "Unable to connect to database" }; } // Database timeout errors if (error.name === 'SequelizeTimeoutError') { return { status: 504, // Gateway Timeout message: "Database timeout", details: "Database operation timed out" }; } // Access denied / authentication errors if (error.name === 'SequelizeAccessDeniedError') { return { status: 403, // Forbidden message: "Database access denied", details: "Insufficient database privileges" }; } // Invalid SQL / database errors if (error.name === 'SequelizeDatabaseError') { // Check for specific database error codes const errorCode = error.parent?.code || error.original?.code; const sqlState = error.parent?.sqlState || error.original?.sqlState; // MySQL/PostgreSQL specific error codes switch (errorCode) { case 'ER_DUP_ENTRY': // MySQL duplicate entry case '23000': // SQL State: Integrity constraint violation case '23505': // PostgreSQL unique violation return { status: 409, // Conflict message: "Resource already exists", details: "Duplicate entry found" }; case 'ER_NO_REFERENCED_ROW_2': // MySQL foreign key constraint case '23503': // PostgreSQL foreign key violation return { status: 422, // Unprocessable Entity message: "Foreign key constraint violation", details: "Referenced resource does not exist" }; case 'ER_BAD_NULL_ERROR': // MySQL not null constraint case '23502': // PostgreSQL not null violation return { status: 422, // Unprocessable Entity message: "Missing required field", details: "Required field cannot be null" }; case 'ER_NO_SUCH_TABLE': // MySQL table doesn't exist case '42P01': // PostgreSQL table doesn't exist return { status: 404, // Not Found message: "Resource not found", details: "Table or view does not exist" }; default: return { status: 400, // Bad Request message: "Database operation failed", details: error.message }; } } // Instance not found (for updates/deletes on non-existent records) if (error.message && error.message.includes('Instance not found')) { return { status: 404, // Not Found message: "Resource not found", details: "The requested resource does not exist" }; } // JSON parsing errors (for JSON fields) if (error.name === 'SyntaxError' && error.message.includes('JSON')) { return { status: 422, // Unprocessable Entity message: "Invalid JSON format", details: error.message }; } // Cast/type conversion errors if (error.message && (error.message.includes('invalid input syntax') || error.message.includes('invalid type'))) { return { status: 422, // Unprocessable Entity message: "Invalid data type", details: error.message }; } // Transaction errors if (error.name === 'SequelizeTransactionError') { return { status: 409, // Conflict message: "Transaction conflict", details: "Transaction could not be completed due to conflicts" }; } // Rate limiting / too many connections if (error.message && (error.message.includes('Too many connections') || error.message.includes('connection limit'))) { return { status: 429, // Too Many Requests message: "Too many connections", details: "Database connection limit exceeded" }; } // Generic bad request for malformed queries if (error.name === 'SequelizeQueryError') { return { status: 400, // Bad Request message: "Invalid query", details: error.message }; } // Return default error for unhandled cases return defaultError; } /** * Creates an error handler middleware with proper HTTP status mapping * @param {Object} responseMessages - Custom response messages * @returns {Function} Express error handler middleware */ function createErrorHandler(responseMessages = {}) { return (err, req, res, next) => { // If error already has status and message, use as-is (for manual errors) if (err.status && err.message && !err.error) { return res.status(err.status).json({ error: err.message, details: err.details }); } // Map database error to appropriate HTTP status const mappedError = mapErrorToHttpStatus(err.error || err); // Use custom message if available, otherwise use mapped message const message = responseMessages[mappedError.status] || mappedError.message; res.status(mappedError.status).json({ error: message, details: mappedError.details }); }; } /** * Enhanced error handler that catches and maps async errors * @param {Function} fn - Async function to wrap * @returns {Function} Express middleware that handles async errors */ function asyncErrorHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; } /** * Handles database errors for specific operations with context * @param {Error} error - Database error * @param {string} operation - Operation type (create, update, delete, etc.) * @param {string} resourceName - Name of the resource being operated on * @returns {Object} Formatted error response */ function handleDatabaseError(error, operation = 'operation', resourceName = 'resource') { const mappedError = mapErrorToHttpStatus(error); // Enhance error messages with context const operationMessages = { create: { 409: `${resourceName} already exists`, 422: `Invalid ${resourceName} data provided`, 500: `Failed to create ${resourceName}` }, update: { 404: `${resourceName} not found`, 409: `${resourceName} conflicts with existing data`, 422: `Invalid ${resourceName} data provided`, 500: `Failed to update ${resourceName}` }, delete: { 404: `${resourceName} not found`, 422: `Cannot delete ${resourceName} due to dependencies`, 500: `Failed to delete ${resourceName}` }, read: { 404: `${resourceName} not found`, 500: `Failed to retrieve ${resourceName}` } }; const contextualMessage = operationMessages[operation]?.[mappedError.status] || mappedError.message; return { ...mappedError, message: contextualMessage }; } module.exports = { mapErrorToHttpStatus, createErrorHandler, asyncErrorHandler, handleDatabaseError };