Skip to content

Commit cf69ebd

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

4 files changed

Lines changed: 270 additions & 11 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright IBM Corp. and LoopBack contributors 2019,2020. 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 {expect} from '@loopback/testlab';
7+
import {DatabaseDriverError, isDatabaseDriverError} from '../../..';
8+
9+
describe('DatabaseDriverError', () => {
10+
it('inherits from Error correctly', () => {
11+
const err = givenAnErrorInstance();
12+
expect(err).to.be.instanceof(DatabaseDriverError);
13+
expect(err).to.be.instanceof(Error);
14+
expect(err.stack)
15+
.to.be.String()
16+
// NOTE(bajtos) We cannot assert using __filename because stack traces
17+
// are typically converted from JS paths to TS paths using source maps.
18+
.and.match(/database-driver-error\.test\.(ts|js)/);
19+
});
20+
21+
it('sets code to "DB_FOREIGN_KEY_VIOLATION"', () => {
22+
const err = givenAnErrorInstance();
23+
expect(err.code).to.equal('DB_FOREIGN_KEY_VIOLATION');
24+
});
25+
26+
it('sets statusCode to 422', () => {
27+
const err = givenAnErrorInstance();
28+
expect(err.statusCode).to.equal(422);
29+
});
30+
31+
it('sets nativeCode to "1216"', () => {
32+
const err = givenAnErrorInstance();
33+
expect(err.nativeCode).to.equal('1216'); // mysql's native code for foreign key violation
34+
});
35+
});
36+
37+
describe('isDatabaseDriverError', () => {
38+
it('returns true for an instance of DatabaseDriverError', () => {
39+
const error = givenAnErrorInstance();
40+
expect(isDatabaseDriverError(error)).to.be.true();
41+
});
42+
43+
it('returns false for an instance of Error', () => {
44+
const error = new Error('A generic error');
45+
expect(isDatabaseDriverError(error)).to.be.false();
46+
});
47+
});
48+
49+
function givenAnErrorInstance() {
50+
return new DatabaseDriverError('User', '', {
51+
code: 'DB_FOREIGN_KEY_VIOLATION',
52+
statusCode: 422,
53+
nativeCode: '1216', // mysql's native code for foreign key violation
54+
});
55+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
}
39+
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
export function isDatabaseDriverError(e: any): e is DatabaseDriverError {
42+
return e instanceof DatabaseDriverError;
43+
}

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: 171 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,156 @@ 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 parsedCode = Number(error.code);
98+
const rawCode = !isNaN(parsedCode) ? error.code : error.errno; // error.code for posgres while errno for mysql/mongodb
99+
100+
const codeStr = String(rawCode);
101+
102+
// Initialize with default values
103+
let statusCode = 500;
104+
let errorCode = 'DATABASE_ERROR';
105+
let message = error.message || 'An unexpected database error occurred.';
106+
107+
// Evaluate database signatures and re-map properties dynamically
108+
switch (codeStr) {
109+
// 1. Unique Key / Duplicate Entries
110+
case '23505': // Postgres
111+
case '1062': // MySQL
112+
case '11000': // MongoDB
113+
case '11001':
114+
statusCode = 409;
115+
errorCode = 'DB_UNIQUE_CONSTRAINT_VIOLATION';
116+
message =
117+
'The operation conflicts with an existing record unique constraint.';
118+
break;
119+
// 2. Foreign Key Constraints (Missing Parents / Existing Children)
120+
case '23503': // Postgres
121+
case '1216': // MySQL
122+
case '1217':
123+
case '1451':
124+
case '1452':
125+
statusCode = 422;
126+
errorCode = 'DB_FOREIGN_KEY_VIOLATION';
127+
message =
128+
'Relational integrity validation failed. Referenced parent record not found.';
129+
break;
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+
errorCode = 'DB_NOT_NULL_VIOLATION';
137+
message = 'Required database schema properties are missing or null.';
138+
break;
139+
// 4. Bad Casts / Truncation / Data Type Mismatch
140+
case '22P02': // Postgres Invalid Text Representation (e.g. Bad UUID format)
141+
case '22001': // Postgres String Data Right Truncation
142+
case '1265': // MySQL Data Truncated
143+
case '1366':
144+
statusCode = 400;
145+
errorCode = 'DB_DATA_TYPE_MISMATCH';
146+
message =
147+
'The query properties contain unexpected formatting types or overflows.';
148+
break;
149+
case '3105': // MySQL Server Generated column value ignored/disallowed
150+
case '1906': // MariaDB Generated column value ignored/disallowed
151+
statusCode = 400;
152+
errorCode = 'DB_GENERATED_COLUMN_VIOLATION';
153+
message =
154+
'Cannot manually assign or update values on a database-generated computed column.';
155+
break;
156+
// 5. Missing Table / Schema Definition Errors
157+
case '1146': // MySQL errno for missing table
158+
case '42P01': // Postgres error code for undefined_table
159+
statusCode = 400; // Setting as 400 because 500 is too generic and doesn't provide enough context for the client
160+
errorCode = 'DB_SCHEMA_MISSING_TABLE';
161+
message = 'The requested database table or relation does not exist.';
162+
break;
163+
// 6. Query Timeout Errors
164+
case '57014': // Postgres query_canceled
165+
case '1907': // MySQL query_timeout
166+
case '3024': // MongoDB query_timeout
167+
statusCode = 504;
168+
errorCode = 'DB_QUERY_TIMEOUT';
169+
message = 'The database operation took too long and was aborted.';
170+
break;
171+
// 7. Concurrency / Locking Conflicts
172+
case '40001': // Postgres serialization_failure
173+
case '40P01': // Postgres deadlock_detected
174+
case '1213': // MySQL deadlock found when trying to get lock
175+
case '1205': // MySQL lock wait timeout exceeded
176+
statusCode = 409;
177+
errorCode = 'DB_LOCK_CONFLICT';
178+
message =
179+
'A concurrency lock conflict occurred. Please retry the operation.';
180+
break;
181+
// 8. Check Constraint / Business Logic Violations
182+
case '23514': // Postgres check_violation
183+
case '23P01': // Postgres exclusion_violation
184+
case '3819': // MySQL check constraint violation
185+
statusCode = 400;
186+
errorCode = 'DB_CHECK_CONSTRAINT_VIOLATION';
187+
message =
188+
'The data violates database business logic or range constraints.';
189+
break;
190+
// 9. Connection / Availability Issues
191+
case '08003': // Postgres connection_does_not_exist
192+
case '08006': // Postgres connection_failure
193+
case '53300': // Postgres too_many_connections
194+
case '1040': // MySQL too many connections
195+
case '2002': // MySQL connection refused
196+
case '2003': // MySQL can't connect to MySQL server
197+
case '2006': // MySQL server has gone away
198+
case '2013': // MySQL lost connection to MySQL server during query
199+
case '8000': // MongoDB network error
200+
case '8001': // MongoDB connection closed
201+
case '8002': // MongoDB connection timeout
202+
statusCode = 503;
203+
errorCode = 'DB_CONNECTION_FAILURE';
204+
message =
205+
'The database is temporarily unavailable or overloaded. Please try again later.';
206+
break;
207+
// 10. Numeric Overflow / Out of Range Values
208+
case '22003': // Postgres numeric_value_out_of_range
209+
case '1264': // MySQL Out of range value for column
210+
statusCode = 400;
211+
errorCode = 'DB_NUMERIC_OUT_OF_RANGE';
212+
message =
213+
'A numeric or string value exceeds the maximum allowable size for the field.';
214+
break;
215+
}
216+
if (error.stack) console.error(error.stack);
217+
218+
// If we matched a standard driver rule, throw our clean uniform class
219+
if (statusCode !== 500) {
220+
throw new DatabaseDriverError(this.entityClass, message, {
221+
code: errorCode,
222+
statusCode: statusCode,
223+
nativeCode: rawCode,
224+
});
225+
}
226+
227+
// Otherwise, bubble up the original error safely to protect core connection strings/etc.
228+
throw err;
229+
}
230+
81231
/**
82232
* This is a bridge to the legacy DAO class. The function mixes DAO methods
83233
* into a model class and attach it to a given data source
@@ -488,7 +638,9 @@ export class DefaultCrudRepository<
488638
async create(entity: DataObject<T>, options?: Options): Promise<T> {
489639
// perform persist hook
490640
const data = await this.entityToData(entity, options);
491-
const model = await ensurePromise(this.modelClass.create(data, options));
641+
const model = await ensurePromise(
642+
this.modelClass.create(data, options),
643+
).catch(handleDatabaseDriverError);
492644
return this.toEntity(model);
493645
}
494646

@@ -499,7 +651,7 @@ export class DefaultCrudRepository<
499651
);
500652
const models = await ensurePromise(
501653
this.modelClass.createAll(data, options),
502-
);
654+
).catch(handleDatabaseDriverError);
503655
return this.toEntities(models);
504656
}
505657

@@ -520,7 +672,7 @@ export class DefaultCrudRepository<
520672
const include = filter?.include;
521673
const models = await ensurePromise(
522674
this.modelClass.find(this.normalizeFilter(filter), options),
523-
);
675+
).catch(handleDatabaseDriverError);
524676
const entities = this.toEntities(models);
525677
return this.includeRelatedModels(entities, include, options);
526678
}
@@ -531,7 +683,7 @@ export class DefaultCrudRepository<
531683
): Promise<(T & Relations) | null> {
532684
const model = await ensurePromise(
533685
this.modelClass.findOne(this.normalizeFilter(filter), options),
534-
);
686+
).catch(handleDatabaseDriverError);
535687
if (!model) return null;
536688
const entity = this.toEntity(model);
537689
const include = filter?.include;
@@ -551,7 +703,7 @@ export class DefaultCrudRepository<
551703
const include = filter?.include;
552704
const model = await ensurePromise(
553705
this.modelClass.findById(id, this.normalizeFilter(filter), options),
554-
);
706+
).catch(handleDatabaseDriverError);
555707
if (!model) {
556708
throw new EntityNotFoundError(this.entityClass, id);
557709
}
@@ -583,7 +735,7 @@ export class DefaultCrudRepository<
583735
const persistedData = await this.entityToData(data, options);
584736
const result = await ensurePromise(
585737
this.modelClass.updateAll(where, persistedData, options),
586-
);
738+
).catch(handleDatabaseDriverError);
587739
return {count: result.count};
588740
}
589741

@@ -614,7 +766,9 @@ export class DefaultCrudRepository<
614766
): Promise<void> {
615767
try {
616768
const payload = await this.entityToData(data, options);
617-
await ensurePromise(this.modelClass.replaceById(id, payload, options));
769+
await ensurePromise(
770+
this.modelClass.replaceById(id, payload, options),
771+
).catch(handleDatabaseDriverError);
618772
} catch (err) {
619773
if (err.statusCode === 404) {
620774
throw new EntityNotFoundError(this.entityClass, id);
@@ -626,24 +780,30 @@ export class DefaultCrudRepository<
626780
async deleteAll(where?: Where<T>, options?: Options): Promise<Count> {
627781
const result = await ensurePromise(
628782
this.modelClass.deleteAll(where, options),
629-
);
783+
).catch(handleDatabaseDriverError);
630784
return {count: result.count};
631785
}
632786

633787
async deleteById(id: ID, options?: Options): Promise<void> {
634-
const result = await ensurePromise(this.modelClass.deleteById(id, options));
788+
const result = await ensurePromise(
789+
this.modelClass.deleteById(id, options),
790+
).catch(handleDatabaseDriverError);
635791
if (result.count === 0) {
636792
throw new EntityNotFoundError(this.entityClass, id);
637793
}
638794
}
639795

640796
async count(where?: Where<T>, options?: Options): Promise<Count> {
641-
const result = await ensurePromise(this.modelClass.count(where, options));
797+
const result = await ensurePromise(
798+
this.modelClass.count(where, options),
799+
).catch(handleDatabaseDriverError);
642800
return {count: result};
643801
}
644802

645803
exists(id: ID, options?: Options): Promise<boolean> {
646-
return ensurePromise(this.modelClass.exists(id, options));
804+
return ensurePromise(this.modelClass.exists(id, options)).catch(
805+
handleDatabaseDriverError,
806+
);
647807
}
648808

649809
/**

0 commit comments

Comments
 (0)