From d626a86d367f5795643c6d15a7ea05d1c475d2d6 Mon Sep 17 00:00:00 2001 From: Muhammad Aaqil Date: Thu, 2 Jul 2026 11:12:15 +0500 Subject: [PATCH] feat: handle database errors Signed-off-by: Muhammad Aaqil --- .../src/errors/database-driver.error.ts | 38 ++++++ packages/repository/src/errors/index.ts | 1 + .../src/repositories/legacy-juggler-bridge.ts | 116 ++++++++++++++++-- 3 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 packages/repository/src/errors/database-driver.error.ts diff --git a/packages/repository/src/errors/database-driver.error.ts b/packages/repository/src/errors/database-driver.error.ts new file mode 100644 index 000000000000..8379b5f1f963 --- /dev/null +++ b/packages/repository/src/errors/database-driver.error.ts @@ -0,0 +1,38 @@ +// Copyright IBM Corp. and LoopBack contributors 2018,2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity} from '../model'; + +export class DatabaseDriverError extends Error { + code: string; + statusCode: number; + entityName: string; + nativeCode: string | number; + + constructor( + entityOrName: typeof Entity | string, + message: string, + options: { + code: string; + statusCode: number; + nativeCode: string | number; + }, + ) { + const entityName = + typeof entityOrName === 'string' + ? entityOrName + : entityOrName.modelName || entityOrName.name; + + super(message); + + this.name = 'DatabaseDriverError'; + this.entityName = entityName; + this.code = options.code; + this.statusCode = options.statusCode; + this.nativeCode = options.nativeCode; + + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/packages/repository/src/errors/index.ts b/packages/repository/src/errors/index.ts index ac99f724d828..196a5a0545c8 100644 --- a/packages/repository/src/errors/index.ts +++ b/packages/repository/src/errors/index.ts @@ -7,3 +7,4 @@ export * from './entity-not-found.error'; export * from './invalid-polymorphism.error'; export * from './invalid-relation.error'; export * from './invalid-body.error'; +export * from './database-driver.error'; diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 025ea10393e0..6e55e5478700 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -78,6 +78,90 @@ function isModelClass( ); } +import {DatabaseDriverError} from '../errors'; + +function handleDatabaseDriverError( + this: DefaultCrudRepository, + err: unknown, +): never { + const error = err as AnyObject; + if (err === null) { + throw new Error('An unknown database execution error occurred.'); + } + + // Handling existing already mapped errors + if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) { + throw error; + } + + const rawCode = error.code || error.sqlState || ''; + const codeStr = String(rawCode); + + // Initialize with default values + let statusCode = 500; + let frameworkCode = 'DATABASE_ERROR'; + let cleanMessage = error.message || 'An unexpected database error occurred.'; + + // Evaluate database signatures and re-map properties dynamically + switch (codeStr) { + // 1. Unique Key / Duplicate Entries + case '23505': // Postgres + case '1062': // MySQL + case '11000': // MongoDB + case '11001': + statusCode = 409; + frameworkCode = 'DB_UNIQUE_CONSTRAINT_VIOLATION'; + cleanMessage = + 'The operation conflicts with an existing record unique constraint.'; + break; + + // 2. Foreign Key Constraints (Missing Parents / Existing Children) + case '23503': // Postgres + case '1216': // MySQL + case '1217': + case '1451': + case '1452': + statusCode = 422; + frameworkCode = 'DB_FOREIGN_KEY_VIOLATION'; + cleanMessage = + 'Relational integrity validation failed. Referenced parent record not found.'; + break; + + // 3. Null / Required Fields Omissions + case '23502': // Postgres + case '1048': // MySQL + case '1364': + case '121': // MongoDB Document Validation Failed + statusCode = 400; + frameworkCode = 'DB_NOT_NULL_VIOLATION'; + cleanMessage = 'Required database schema properties are missing or null.'; + break; + + // 4. Bad Casts / Truncation / Data Type Mismatch + case '22P02': // Postgres Invalid Text Representation (e.g. Bad UUID format) + case '22001': // Postgres String Data Right Truncation + case '1265': // MySQL Data Truncated + case '1366': + statusCode = 400; + frameworkCode = 'DB_DATA_TYPE_MISMATCH'; + cleanMessage = + 'The query properties contain unexpected formatting types or overflows.'; + break; + } + + // If we matched a standard driver rule, throw our clean uniform class + if (statusCode !== 500) { + throw new DatabaseDriverError(this.entityClass, cleanMessage, { + code: frameworkCode, + statusCode: statusCode, + nativeCode: rawCode, + }); + } + + // Otherwise, bubble up the original error safely to protect core connection strings/etc. + throw err; +} + /** * This is a bridge to the legacy DAO class. The function mixes DAO methods * into a model class and attach it to a given data source @@ -488,7 +572,9 @@ export class DefaultCrudRepository< async create(entity: DataObject, options?: Options): Promise { // perform persist hook const data = await this.entityToData(entity, options); - const model = await ensurePromise(this.modelClass.create(data, options)); + const model = await ensurePromise( + this.modelClass.create(data, options), + ).catch(handleDatabaseDriverError); return this.toEntity(model); } @@ -499,7 +585,7 @@ export class DefaultCrudRepository< ); const models = await ensurePromise( this.modelClass.createAll(data, options), - ); + ).catch(handleDatabaseDriverError); return this.toEntities(models); } @@ -520,7 +606,7 @@ export class DefaultCrudRepository< const include = filter?.include; const models = await ensurePromise( this.modelClass.find(this.normalizeFilter(filter), options), - ); + ).catch(handleDatabaseDriverError); const entities = this.toEntities(models); return this.includeRelatedModels(entities, include, options); } @@ -531,7 +617,7 @@ export class DefaultCrudRepository< ): Promise<(T & Relations) | null> { const model = await ensurePromise( this.modelClass.findOne(this.normalizeFilter(filter), options), - ); + ).catch(handleDatabaseDriverError); if (!model) return null; const entity = this.toEntity(model); const include = filter?.include; @@ -551,7 +637,7 @@ export class DefaultCrudRepository< const include = filter?.include; const model = await ensurePromise( this.modelClass.findById(id, this.normalizeFilter(filter), options), - ); + ).catch(handleDatabaseDriverError); if (!model) { throw new EntityNotFoundError(this.entityClass, id); } @@ -583,7 +669,7 @@ export class DefaultCrudRepository< const persistedData = await this.entityToData(data, options); const result = await ensurePromise( this.modelClass.updateAll(where, persistedData, options), - ); + ).catch(handleDatabaseDriverError); return {count: result.count}; } @@ -614,7 +700,9 @@ export class DefaultCrudRepository< ): Promise { try { const payload = await this.entityToData(data, options); - await ensurePromise(this.modelClass.replaceById(id, payload, options)); + await ensurePromise( + this.modelClass.replaceById(id, payload, options), + ).catch(handleDatabaseDriverError); } catch (err) { if (err.statusCode === 404) { throw new EntityNotFoundError(this.entityClass, id); @@ -626,24 +714,30 @@ export class DefaultCrudRepository< async deleteAll(where?: Where, options?: Options): Promise { const result = await ensurePromise( this.modelClass.deleteAll(where, options), - ); + ).catch(handleDatabaseDriverError); return {count: result.count}; } async deleteById(id: ID, options?: Options): Promise { - const result = await ensurePromise(this.modelClass.deleteById(id, options)); + const result = await ensurePromise( + this.modelClass.deleteById(id, options), + ).catch(handleDatabaseDriverError); if (result.count === 0) { throw new EntityNotFoundError(this.entityClass, id); } } async count(where?: Where, options?: Options): Promise { - const result = await ensurePromise(this.modelClass.count(where, options)); + const result = await ensurePromise( + this.modelClass.count(where, options), + ).catch(handleDatabaseDriverError); return {count: result}; } exists(id: ID, options?: Options): Promise { - return ensurePromise(this.modelClass.exists(id, options)); + return ensurePromise(this.modelClass.exists(id, options)).catch( + handleDatabaseDriverError, + ); } /**