Skip to content

Commit 5240504

Browse files
committed
feat: handle database errors
Signed-off-by: Muhammad Aaqil <aaqilcs102@gmail.com>
1 parent d1838ad commit 5240504

3 files changed

Lines changed: 144 additions & 11 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright IBM Corp. and LoopBack contributors 2018,2019. All Rights Reserved.
2+
// Node module: @loopback/repository
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {Entity} from '../model';
7+
8+
export class DatabaseDriverError extends Error {
9+
code: string;
10+
statusCode: number;
11+
entityName: string;
12+
nativeCode: string | number;
13+
14+
constructor(
15+
entityOrName: typeof Entity | string,
16+
message: string,
17+
options: {
18+
code: string;
19+
statusCode: number;
20+
nativeCode: string | number;
21+
},
22+
) {
23+
const entityName =
24+
typeof entityOrName === 'string'
25+
? entityOrName
26+
: entityOrName.modelName || entityOrName.name;
27+
28+
super(message);
29+
30+
this.name = 'DatabaseDriverError';
31+
this.entityName = entityName;
32+
this.code = options.code;
33+
this.statusCode = options.statusCode;
34+
this.nativeCode = options.nativeCode;
35+
36+
Error.captureStackTrace(this, this.constructor);
37+
}
38+
}

packages/repository/src/errors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './entity-not-found.error';
77
export * from './invalid-polymorphism.error';
88
export * from './invalid-relation.error';
99
export * from './invalid-body.error';
10+
export * from './database-driver.error';

packages/repository/src/repositories/legacy-juggler-bridge.ts

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,90 @@ function isModelClass(
7878
);
7979
}
8080

81+
import {DatabaseDriverError} from '../errors';
82+
83+
function handleDatabaseDriverError(
84+
this: DefaultCrudRepository<Entity, unknown, AnyObject>,
85+
err: unknown,
86+
): never {
87+
const error = err as AnyObject;
88+
if (err === null || err === undefined) {
89+
throw new Error('An unknown database execution error occurred.');
90+
}
91+
92+
// Handling existing already mapped errors
93+
if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
94+
throw error;
95+
}
96+
97+
const rawCode = error.code || error.sqlState || '';
98+
const codeStr = String(rawCode);
99+
100+
// Initialize with default values
101+
let statusCode = 500;
102+
let frameworkCode = 'DATABASE_ERROR';
103+
let cleanMessage = error.message || 'An unexpected database error occurred.';
104+
105+
// Evaluate database signatures and re-map properties dynamically
106+
switch (codeStr) {
107+
// 1. Unique Key / Duplicate Entries
108+
case '23505': // Postgres
109+
case '1062': // MySQL
110+
case '11000': // MongoDB
111+
case '11001':
112+
statusCode = 409;
113+
frameworkCode = 'DB_UNIQUE_CONSTRAINT_VIOLATION';
114+
cleanMessage =
115+
'The operation conflicts with an existing record unique constraint.';
116+
break;
117+
118+
// 2. Foreign Key Constraints (Missing Parents / Existing Children)
119+
case '23503': // Postgres
120+
case '1216': // MySQL
121+
case '1217':
122+
case '1451':
123+
case '1452':
124+
statusCode = 422;
125+
frameworkCode = 'DB_FOREIGN_KEY_VIOLATION';
126+
cleanMessage =
127+
'Relational integrity validation failed. Referenced parent record not found.';
128+
break;
129+
130+
// 3. Null / Required Fields Omissions
131+
case '23502': // Postgres
132+
case '1048': // MySQL
133+
case '1364':
134+
case '121': // MongoDB Document Validation Failed
135+
statusCode = 400;
136+
frameworkCode = 'DB_NOT_NULL_VIOLATION';
137+
cleanMessage = 'Required database schema properties are missing or null.';
138+
break;
139+
140+
// 4. Bad Casts / Truncation / Data Type Mismatch
141+
case '22P02': // Postgres Invalid Text Representation (e.g. Bad UUID format)
142+
case '22001': // Postgres String Data Right Truncation
143+
case '1265': // MySQL Data Truncated
144+
case '1366':
145+
statusCode = 400;
146+
frameworkCode = 'DB_DATA_TYPE_MISMATCH';
147+
cleanMessage =
148+
'The query properties contain unexpected formatting types or overflows.';
149+
break;
150+
}
151+
152+
// If we matched a standard driver rule, throw our clean uniform class
153+
if (statusCode !== 500) {
154+
throw new DatabaseDriverError(this.entityClass, cleanMessage, {
155+
code: frameworkCode,
156+
statusCode: statusCode,
157+
nativeCode: rawCode,
158+
});
159+
}
160+
161+
// Otherwise, bubble up the original error safely to protect core connection strings/etc.
162+
throw err;
163+
}
164+
81165
/**
82166
* This is a bridge to the legacy DAO class. The function mixes DAO methods
83167
* into a model class and attach it to a given data source
@@ -488,7 +572,9 @@ export class DefaultCrudRepository<
488572
async create(entity: DataObject<T>, options?: Options): Promise<T> {
489573
// perform persist hook
490574
const data = await this.entityToData(entity, options);
491-
const model = await ensurePromise(this.modelClass.create(data, options));
575+
const model = await ensurePromise(
576+
this.modelClass.create(data, options),
577+
).catch(handleDatabaseDriverError);
492578
return this.toEntity(model);
493579
}
494580

@@ -499,7 +585,7 @@ export class DefaultCrudRepository<
499585
);
500586
const models = await ensurePromise(
501587
this.modelClass.createAll(data, options),
502-
);
588+
).catch(handleDatabaseDriverError);
503589
return this.toEntities(models);
504590
}
505591

@@ -520,7 +606,7 @@ export class DefaultCrudRepository<
520606
const include = filter?.include;
521607
const models = await ensurePromise(
522608
this.modelClass.find(this.normalizeFilter(filter), options),
523-
);
609+
).catch(handleDatabaseDriverError);
524610
const entities = this.toEntities(models);
525611
return this.includeRelatedModels(entities, include, options);
526612
}
@@ -531,7 +617,7 @@ export class DefaultCrudRepository<
531617
): Promise<(T & Relations) | null> {
532618
const model = await ensurePromise(
533619
this.modelClass.findOne(this.normalizeFilter(filter), options),
534-
);
620+
).catch(handleDatabaseDriverError);
535621
if (!model) return null;
536622
const entity = this.toEntity(model);
537623
const include = filter?.include;
@@ -551,7 +637,7 @@ export class DefaultCrudRepository<
551637
const include = filter?.include;
552638
const model = await ensurePromise(
553639
this.modelClass.findById(id, this.normalizeFilter(filter), options),
554-
);
640+
).catch(handleDatabaseDriverError);
555641
if (!model) {
556642
throw new EntityNotFoundError(this.entityClass, id);
557643
}
@@ -583,7 +669,7 @@ export class DefaultCrudRepository<
583669
const persistedData = await this.entityToData(data, options);
584670
const result = await ensurePromise(
585671
this.modelClass.updateAll(where, persistedData, options),
586-
);
672+
).catch(handleDatabaseDriverError);
587673
return {count: result.count};
588674
}
589675

@@ -614,7 +700,9 @@ export class DefaultCrudRepository<
614700
): Promise<void> {
615701
try {
616702
const payload = await this.entityToData(data, options);
617-
await ensurePromise(this.modelClass.replaceById(id, payload, options));
703+
await ensurePromise(
704+
this.modelClass.replaceById(id, payload, options),
705+
).catch(handleDatabaseDriverError);
618706
} catch (err) {
619707
if (err.statusCode === 404) {
620708
throw new EntityNotFoundError(this.entityClass, id);
@@ -626,24 +714,30 @@ export class DefaultCrudRepository<
626714
async deleteAll(where?: Where<T>, options?: Options): Promise<Count> {
627715
const result = await ensurePromise(
628716
this.modelClass.deleteAll(where, options),
629-
);
717+
).catch(handleDatabaseDriverError);
630718
return {count: result.count};
631719
}
632720

633721
async deleteById(id: ID, options?: Options): Promise<void> {
634-
const result = await ensurePromise(this.modelClass.deleteById(id, options));
722+
const result = await ensurePromise(
723+
this.modelClass.deleteById(id, options),
724+
).catch(handleDatabaseDriverError);
635725
if (result.count === 0) {
636726
throw new EntityNotFoundError(this.entityClass, id);
637727
}
638728
}
639729

640730
async count(where?: Where<T>, options?: Options): Promise<Count> {
641-
const result = await ensurePromise(this.modelClass.count(where, options));
731+
const result = await ensurePromise(
732+
this.modelClass.count(where, options),
733+
).catch(handleDatabaseDriverError);
642734
return {count: result};
643735
}
644736

645737
exists(id: ID, options?: Options): Promise<boolean> {
646-
return ensurePromise(this.modelClass.exists(id, options));
738+
return ensurePromise(this.modelClass.exists(id, options)).catch(
739+
handleDatabaseDriverError,
740+
);
647741
}
648742

649743
/**

0 commit comments

Comments
 (0)