Skip to content

Commit dc6c5f5

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

4 files changed

Lines changed: 211 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: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,97 @@ 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+
// 5. Generated Column Violations
150+
case '3105': // MySQL Server Generated column value ignored/disallowed
151+
case '1906': // MariaDB Generated column value ignored/disallowed
152+
statusCode = 400;
153+
errorCode = 'DB_GENERATED_COLUMN_VIOLATION';
154+
message =
155+
'Cannot manually assign or update values on a database-generated computed column.';
156+
break;
157+
}
158+
159+
// If we matched a standard driver rule, throw our clean uniform class
160+
if (statusCode !== 500) {
161+
throw new DatabaseDriverError(this.entityClass, message, {
162+
code: errorCode,
163+
statusCode: statusCode,
164+
nativeCode: rawCode,
165+
});
166+
}
167+
168+
// Otherwise, bubble up the original error safely to protect core connection strings/etc.
169+
throw err;
170+
}
171+
81172
/**
82173
* This is a bridge to the legacy DAO class. The function mixes DAO methods
83174
* into a model class and attach it to a given data source
@@ -488,7 +579,9 @@ export class DefaultCrudRepository<
488579
async create(entity: DataObject<T>, options?: Options): Promise<T> {
489580
// perform persist hook
490581
const data = await this.entityToData(entity, options);
491-
const model = await ensurePromise(this.modelClass.create(data, options));
582+
const model = await ensurePromise(
583+
this.modelClass.create(data, options),
584+
).catch(handleDatabaseDriverError);
492585
return this.toEntity(model);
493586
}
494587

@@ -499,7 +592,7 @@ export class DefaultCrudRepository<
499592
);
500593
const models = await ensurePromise(
501594
this.modelClass.createAll(data, options),
502-
);
595+
).catch(handleDatabaseDriverError);
503596
return this.toEntities(models);
504597
}
505598

@@ -520,7 +613,7 @@ export class DefaultCrudRepository<
520613
const include = filter?.include;
521614
const models = await ensurePromise(
522615
this.modelClass.find(this.normalizeFilter(filter), options),
523-
);
616+
).catch(handleDatabaseDriverError);
524617
const entities = this.toEntities(models);
525618
return this.includeRelatedModels(entities, include, options);
526619
}
@@ -531,7 +624,7 @@ export class DefaultCrudRepository<
531624
): Promise<(T & Relations) | null> {
532625
const model = await ensurePromise(
533626
this.modelClass.findOne(this.normalizeFilter(filter), options),
534-
);
627+
).catch(handleDatabaseDriverError);
535628
if (!model) return null;
536629
const entity = this.toEntity(model);
537630
const include = filter?.include;
@@ -551,7 +644,7 @@ export class DefaultCrudRepository<
551644
const include = filter?.include;
552645
const model = await ensurePromise(
553646
this.modelClass.findById(id, this.normalizeFilter(filter), options),
554-
);
647+
).catch(handleDatabaseDriverError);
555648
if (!model) {
556649
throw new EntityNotFoundError(this.entityClass, id);
557650
}
@@ -583,7 +676,7 @@ export class DefaultCrudRepository<
583676
const persistedData = await this.entityToData(data, options);
584677
const result = await ensurePromise(
585678
this.modelClass.updateAll(where, persistedData, options),
586-
);
679+
).catch(handleDatabaseDriverError);
587680
return {count: result.count};
588681
}
589682

@@ -614,7 +707,9 @@ export class DefaultCrudRepository<
614707
): Promise<void> {
615708
try {
616709
const payload = await this.entityToData(data, options);
617-
await ensurePromise(this.modelClass.replaceById(id, payload, options));
710+
await ensurePromise(
711+
this.modelClass.replaceById(id, payload, options),
712+
).catch(handleDatabaseDriverError);
618713
} catch (err) {
619714
if (err.statusCode === 404) {
620715
throw new EntityNotFoundError(this.entityClass, id);
@@ -626,24 +721,30 @@ export class DefaultCrudRepository<
626721
async deleteAll(where?: Where<T>, options?: Options): Promise<Count> {
627722
const result = await ensurePromise(
628723
this.modelClass.deleteAll(where, options),
629-
);
724+
).catch(handleDatabaseDriverError);
630725
return {count: result.count};
631726
}
632727

633728
async deleteById(id: ID, options?: Options): Promise<void> {
634-
const result = await ensurePromise(this.modelClass.deleteById(id, options));
729+
const result = await ensurePromise(
730+
this.modelClass.deleteById(id, options),
731+
).catch(handleDatabaseDriverError);
635732
if (result.count === 0) {
636733
throw new EntityNotFoundError(this.entityClass, id);
637734
}
638735
}
639736

640737
async count(where?: Where<T>, options?: Options): Promise<Count> {
641-
const result = await ensurePromise(this.modelClass.count(where, options));
738+
const result = await ensurePromise(
739+
this.modelClass.count(where, options),
740+
).catch(handleDatabaseDriverError);
642741
return {count: result};
643742
}
644743

645744
exists(id: ID, options?: Options): Promise<boolean> {
646-
return ensurePromise(this.modelClass.exists(id, options));
745+
return ensurePromise(this.modelClass.exists(id, options)).catch(
746+
handleDatabaseDriverError,
747+
);
647748
}
648749

649750
/**

0 commit comments

Comments
 (0)