Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 59s
200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
// src/services/db/errors.db.ts
|
|
import type { Logger } from 'pino';
|
|
import { DatabaseError as ProcessingDatabaseError } from '../processingErrors';
|
|
|
|
/**
|
|
* Base class for custom repository-level errors to ensure they have a status property.
|
|
*/
|
|
export class RepositoryError extends Error {
|
|
public status: number;
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message);
|
|
this.name = this.constructor.name;
|
|
this.status = status;
|
|
// This is necessary to make `instanceof` work correctly with transpiled TS classes
|
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a unique constraint is violated (e.g., trying to register an existing email).
|
|
* Corresponds to PostgreSQL error code '23505'.
|
|
*/
|
|
export class UniqueConstraintError extends RepositoryError {
|
|
constructor(message = 'The record already exists.') {
|
|
super(message, 409); // 409 Conflict
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a foreign key constraint is violated (e.g., trying to reference a non-existent record).
|
|
* Corresponds to PostgreSQL error code '23503'.
|
|
*/
|
|
export class ForeignKeyConstraintError extends RepositoryError {
|
|
constructor(message = 'The referenced record does not exist.') {
|
|
super(message, 400); // 400 Bad Request
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a 'not null' constraint is violated.
|
|
* Corresponds to PostgreSQL error code '23502'.
|
|
*/
|
|
export class NotNullConstraintError extends RepositoryError {
|
|
constructor(message = 'A required field was left null.') {
|
|
super(message, 400); // 400 Bad Request
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a 'check' constraint is violated.
|
|
* Corresponds to PostgreSQL error code '23514'.
|
|
*/
|
|
export class CheckConstraintError extends RepositoryError {
|
|
constructor(message = 'A check constraint was violated.') {
|
|
super(message, 400); // 400 Bad Request
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a value has an invalid text representation for its data type (e.g., 'abc' for an integer).
|
|
* Corresponds to PostgreSQL error code '22P02'.
|
|
*/
|
|
export class InvalidTextRepresentationError extends RepositoryError {
|
|
constructor(message = 'A value has an invalid format for its data type.') {
|
|
super(message, 400); // 400 Bad Request
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a numeric value is out of range for its data type (e.g., too large for an integer).
|
|
* Corresponds to PostgreSQL error code '22003'.
|
|
*/
|
|
export class NumericValueOutOfRangeError extends RepositoryError {
|
|
constructor(message = 'A numeric value is out of the allowed range.') {
|
|
super(message, 400); // 400 Bad Request
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when a specific record is not found in the database.
|
|
*/
|
|
export class NotFoundError extends RepositoryError {
|
|
constructor(message = 'The requested resource was not found.') {
|
|
super(message, 404); // 404 Not Found
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Thrown when the user does not have permission to access the resource.
|
|
*/
|
|
export class ForbiddenError extends RepositoryError {
|
|
constructor(message = 'Access denied.') {
|
|
super(message, 403); // 403 Forbidden
|
|
this.name = 'ForbiddenError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Defines the structure for a single validation issue, often from a library like Zod.
|
|
*/
|
|
export interface ValidationIssue {
|
|
path: (string | number)[];
|
|
message: string;
|
|
[key: string]: unknown; // Allow other properties that might exist on the error object
|
|
}
|
|
|
|
/**
|
|
* Thrown when request validation fails (e.g., missing body fields or invalid params).
|
|
*/
|
|
export class ValidationError extends RepositoryError {
|
|
public validationErrors: ValidationIssue[];
|
|
|
|
constructor(errors: ValidationIssue[], message = 'The request data is invalid.') {
|
|
super(message, 400); // 400 Bad Request
|
|
this.name = 'ValidationError';
|
|
this.validationErrors = errors;
|
|
}
|
|
}
|
|
|
|
export class FileUploadError extends Error {
|
|
public status = 400;
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'FileUploadError';
|
|
}
|
|
}
|
|
|
|
export interface HandleDbErrorOptions {
|
|
entityName?: string;
|
|
uniqueMessage?: string;
|
|
fkMessage?: string;
|
|
notNullMessage?: string;
|
|
checkMessage?: string;
|
|
invalidTextMessage?: string;
|
|
numericOutOfRangeMessage?: string;
|
|
defaultMessage?: string;
|
|
}
|
|
|
|
/**
|
|
* A type guard to check if an error object is a PostgreSQL error with a code.
|
|
*/
|
|
function isPostgresError(
|
|
error: unknown,
|
|
): error is { code: string; constraint?: string; detail?: string } {
|
|
return typeof error === 'object' && error !== null && 'code' in error;
|
|
}
|
|
|
|
/**
|
|
* Centralized error handler for database repositories.
|
|
* Logs the error and throws appropriate custom errors based on PostgreSQL error codes.
|
|
*/
|
|
export function handleDbError(
|
|
error: unknown,
|
|
logger: Logger,
|
|
logMessage: string,
|
|
logContext: Record<string, unknown>,
|
|
options: HandleDbErrorOptions = {},
|
|
): never {
|
|
// If it's already a known domain error (like NotFoundError thrown manually), rethrow it.
|
|
if (error instanceof RepositoryError) {
|
|
throw error;
|
|
}
|
|
|
|
if (isPostgresError(error)) {
|
|
const { code, constraint, detail } = error;
|
|
const enhancedLogContext = { err: error, code, constraint, detail, ...logContext };
|
|
|
|
// Log the detailed error first
|
|
logger.error(enhancedLogContext, logMessage);
|
|
|
|
// Now, throw the appropriate custom error
|
|
switch (code) {
|
|
case '23505': // unique_violation
|
|
throw new UniqueConstraintError(options.uniqueMessage);
|
|
case '23503': // foreign_key_violation
|
|
throw new ForeignKeyConstraintError(options.fkMessage);
|
|
case '23502': // not_null_violation
|
|
throw new NotNullConstraintError(options.notNullMessage);
|
|
case '23514': // check_violation
|
|
throw new CheckConstraintError(options.checkMessage);
|
|
case '22P02': // invalid_text_representation
|
|
throw new InvalidTextRepresentationError(options.invalidTextMessage);
|
|
case '22003': // numeric_value_out_of_range
|
|
throw new NumericValueOutOfRangeError(options.numericOutOfRangeMessage);
|
|
default:
|
|
// If it's a PG error but not one we handle specifically, fall through to the generic error.
|
|
break;
|
|
}
|
|
} else {
|
|
// Log the error if it wasn't a recognized Postgres error
|
|
logger.error({ err: error, ...logContext }, logMessage);
|
|
}
|
|
|
|
// Fallback generic error
|
|
// Use the consistent DatabaseError from the processing errors module for the fallback.
|
|
const errorMessage = options.defaultMessage || `Failed to perform operation on ${options.entityName || 'database'}.`;
|
|
throw new ProcessingDatabaseError(errorMessage);
|
|
}
|