Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/repository/src/errors/database-driver.error.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions packages/repository/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
116 changes: 105 additions & 11 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,90 @@ function isModelClass(
);
}

import {DatabaseDriverError} from '../errors';

function handleDatabaseDriverError(
this: DefaultCrudRepository<Entity, unknown, AnyObject>,
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
Expand Down Expand Up @@ -488,7 +572,9 @@ export class DefaultCrudRepository<
async create(entity: DataObject<T>, options?: Options): Promise<T> {
// 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);
}

Expand All @@ -499,7 +585,7 @@ export class DefaultCrudRepository<
);
const models = await ensurePromise(
this.modelClass.createAll(data, options),
);
).catch(handleDatabaseDriverError);
return this.toEntities(models);
}

Expand All @@ -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);
}
Expand All @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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};
}

Expand Down Expand Up @@ -614,7 +700,9 @@ export class DefaultCrudRepository<
): Promise<void> {
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);
Expand All @@ -626,24 +714,30 @@ export class DefaultCrudRepository<
async deleteAll(where?: Where<T>, options?: Options): Promise<Count> {
const result = await ensurePromise(
this.modelClass.deleteAll(where, options),
);
).catch(handleDatabaseDriverError);
return {count: result.count};
}

async deleteById(id: ID, options?: Options): Promise<void> {
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<T>, options?: Options): Promise<Count> {
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<boolean> {
return ensurePromise(this.modelClass.exists(id, options));
return ensurePromise(this.modelClass.exists(id, options)).catch(
handleDatabaseDriverError,
);
}

/**
Expand Down
Loading