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
JavaScript
/**
* 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
};