All files / src/services/db errors.db.ts

86.44% Statements 204/236
81.25% Branches 52/64
82.14% Functions 23/28
84.42% Lines 168/199

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 2002x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 496x 496x 496x 2x 496x         2x 2x 2x 2x 2x 18x 2x 2x 2x 2x 2x 2x 2x 2x 2x 37x       2x 2x 2x 2x 2x 2x 3x 2x 2x 2x 2x 2x 20x 20x 20x 20x 9x 2x 2x 2x 2x 2x 2x 2x 2x 2x 3x 2x 2x 2x 2x 2x 2x   2x 2x 3x         2x 2x 2x   202x 2x 2x 2x 2x 2x 7x 2x 2x 25x 25x 6x 6x 6x 6x 2x 4x 4x 4x 4x 2x           2x 3x 3x 2x 192x 192x 192x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 3x     3x 2x 2x 2x 2x 3x     3x   211x       3x 8x           8x 8x 8x 2x 265x 54x 2x 2x 211x 49x 49x 2x 2x 49x 2x 2x 49x 2x 12x 2x 26x 2x 2x 2x 7x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 162x 2x 2x 2x 2x 163x 265x 2x  
// 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);
}