Files
flyer-crawler.projectium.com/src/services/db/errors.db.ts
Torben Sorensen e86e09703e
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 59s
even even more and more test fixes
2026-01-05 11:27:13 -08:00

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);
}