Skip to content
Open
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
21 changes: 21 additions & 0 deletions docs/site/reference/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ You have the following options how to fix the error
}
```

## Database Error Codes

When repository operations fail due to database constraints, connection issues,
or protocol errors, LoopBack maps low-level database driver errors to
standardized, protocol-neutral error codes.

These domain error codes are automatically mapped to corresponding REST HTTP
statuses by `@loopback/rest`:

| Error Code | HTTP Status | Description |
| :---------------------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `UNIQUE_CONSTRAINT_VIOLATION` | 409 Conflict | A record with a duplicate key or non-unique value was inserted or updated. |
| `LOCK_CONFLICT` | 409 Conflict | The operation failed due to a database deadlock or lock wait timeout. |
| `FOREIGN_KEY_VIOLATION` | 422 Unprocessable Entity | The operation violates a foreign key constraint (referencing a non-existent parent or deleting a referenced parent). |
| `NOT_NULL_VIOLATION` | 400 Bad Request | A required non-nullable database column was provided as `null` or omitted without a default value. |
| `CHECK_CONSTRAINT_VIOLATION` | 400 Bad Request | The operation violates a custom SQL `CHECK` constraint. |
| `DATA_TYPE_MISMATCH` | 400 Bad Request | A value provided does not match the database column type (e.g., string length exceeded, invalid string format for field type). |
| `GENERATED_COLUMN_VIOLATION` | 400 Bad Request | An explicit attempt was made to write or update a generated/computed database column. |
| `QUERY_TIMEOUT` | 504 Gateway Timeout | The database query execution exceeded the configured statement timeout threshold. |
| `CONNECTION_FAILURE` | 503 Service Unavailable | The database connection was lost, refused, or unable to be acquired from the pool. |

## Other error codes

Besides LoopBack-specific error codes, your application can encounter low-level
Expand Down
2 changes: 1 addition & 1 deletion examples/todo/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function givenTodo(todo?: Partial<Todo>) {

export const aLocation = {
address: '1 New Orchard Road, Armonk, 10504',
geopoint: <GeoPoint>{y: 41.109728357749, x: -73.72462031805},
geopoint: <GeoPoint>{y: 41.109725723771, x: -73.724620709372},
get geostring() {
return `${this.geopoint.y},${this.geopoint.x}`;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright IBM Corp. and LoopBack contributors 2019,2020. 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 {expect} from '@loopback/testlab';

import {
CheckConstraintError,
DatabaseConnectionError,
DatabaseError,
DataTypeMismatchError,
ForeignKeyConstraintError,
LockConflictError,
NotNullConstraintError,
UniqueConstraintError,
} from '../../..';
import {handleRepositoryError} from '../../../errors/handle-repository-error';

describe('handleRepositoryError', () => {
it('throws DatabaseError for a falsy error', () => {
expect(() => handleRepositoryError(null)).to.throwError(
new DatabaseError('An unknown database execution error occurred.'),
);
});

it('passes through an existing DatabaseError', () => {
const error = new DatabaseError('Database error');

expect(() => handleRepositoryError(error)).to.throwError(error);
});

it('maps UNIQUE_CONSTRAINT_VIOLATION', () => {
const error = givenConnectorError(
'UNIQUE_CONSTRAINT_VIOLATION',
'Duplicate value',
);

expect(() => handleRepositoryError(error)).to.throwError(
UniqueConstraintError,
);

expect(() => handleRepositoryError(error)).to.throwError(/Duplicate value/);
});

it('maps FOREIGN_KEY_VIOLATION', () => {
const error = givenConnectorError(
'FOREIGN_KEY_VIOLATION',
'Foreign key violation',
);

expect(() => handleRepositoryError(error)).to.throwError(
ForeignKeyConstraintError,
);
});

it('maps NOT_NULL_VIOLATION', () => {
const error = givenConnectorError(
'NOT_NULL_VIOLATION',
'Not-null violation',
);

expect(() => handleRepositoryError(error)).to.throwError(
NotNullConstraintError,
);
});

it('maps CHECK_CONSTRAINT_VIOLATION', () => {
const error = givenConnectorError(
'CHECK_CONSTRAINT_VIOLATION',
'Check constraint violation',
);

expect(() => handleRepositoryError(error)).to.throwError(
CheckConstraintError,
);
});

it('maps DATA_TYPE_MISMATCH', () => {
const error = givenConnectorError(
'DATA_TYPE_MISMATCH',
'Data type mismatch',
);

expect(() => handleRepositoryError(error)).to.throwError(
DataTypeMismatchError,
);
});

it('maps LOCK_CONFLICT', () => {
const error = givenConnectorError('LOCK_CONFLICT', 'Lock conflict');

expect(() => handleRepositoryError(error)).to.throwError(LockConflictError);
});

it('maps CONNECTION_FAILURE', () => {
const error = givenConnectorError(
'CONNECTION_FAILURE',
'Connection failure',
);

expect(() => handleRepositoryError(error)).to.throwError(
DatabaseConnectionError,
);
});

it('preserves message and details when mapping an error', () => {
const details = {
constraint: 'users_email_key',
field: 'email',
};

const error = {
code: 'UNIQUE_CONSTRAINT_VIOLATION',
message: 'Email already exists',
details,
};

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.be.instanceof(UniqueConstraintError);
expect((err as UniqueConstraintError).message).to.equal(
'Email already exists',
);
expect((err as UniqueConstraintError).details).to.eql(details);
}
});

it('rethrows an unmapped connector error as-is', () => {
const error = {
code: 'SOME_UNKNOWN_ERROR',
message: 'Unknown connector error',
};

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.equal(error);
}
});

it('rethrows a generic Error as-is', () => {
const error = new Error('Something went wrong');

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.equal(error);
}
});

it('rethrows an object without a code as-is', () => {
const error = {
message: 'Something went wrong',
};

try {
handleRepositoryError(error);
} catch (err) {
expect(err).to.equal(error);
}
});
});

function givenConnectorError(code: string, message: string) {
return {
code,
message,
details: {
source: 'test',
},
};
}
98 changes: 98 additions & 0 deletions packages/repository/src/errors/database.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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

/**
* Base abstract class for all database-related domain errors.
* Protocol-neutral: Contains no HTTP status codes or transport metadata.
*/
export class DatabaseError extends Error {
public readonly code: string;
public readonly details?: Record<string, unknown>;

constructor(
message: string,
code = 'DATABASE_ERROR',
details?: Record<string, unknown>,
) {
super(message);
this.name = 'DatabaseError';
this.code = code;
this.details = details;

if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}

export class UniqueConstraintError extends DatabaseError {
constructor(
message = 'Unique constraint violation occurred.',
details?: Record<string, unknown>,
) {
super(message, 'UNIQUE_CONSTRAINT_VIOLATION', details);
this.name = 'UniqueConstraintError';
}
}

export class ForeignKeyConstraintError extends DatabaseError {
constructor(
message = 'Foreign key constraint violation occurred.',
details?: Record<string, unknown>,
) {
super(message, 'FOREIGN_KEY_VIOLATION', details);
this.name = 'ForeignKeyConstraintError';
}
}

export class NotNullConstraintError extends DatabaseError {
constructor(
message = 'Required property cannot be null or omitted.',
details?: Record<string, unknown>,
) {
super(message, 'NOT_NULL_VIOLATION', details);
this.name = 'NotNullConstraintError';
}
}

export class CheckConstraintError extends DatabaseError {
constructor(
message = 'Check constraint or validation check failed.',
details?: Record<string, unknown>,
) {
super(message, 'CHECK_CONSTRAINT_VIOLATION', details);
this.name = 'CheckConstraintError';
}
}

export class DataTypeMismatchError extends DatabaseError {
constructor(
message = 'Data type mismatch or string truncation occurred.',
details?: Record<string, unknown>,
) {
super(message, 'DATA_TYPE_MISMATCH', details);
this.name = 'DataTypeMismatchError';
}
}

export class LockConflictError extends DatabaseError {
constructor(
message = 'Concurrency lock conflict or deadlock detected.',
details?: Record<string, unknown>,
) {
super(message, 'LOCK_CONFLICT', details);
this.name = 'LockConflictError';
}
}

export class DatabaseConnectionError extends DatabaseError {
constructor(
message = 'Database service connection failed or unavailable.',
details?: Record<string, unknown>,
) {
super(message, 'CONNECTION_FAILURE', details);
this.name = 'DatabaseConnectionError';
}
}
57 changes: 57 additions & 0 deletions packages/repository/src/errors/handle-repository-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
CheckConstraintError,
DatabaseConnectionError,
DatabaseError,
DataTypeMismatchError,
ForeignKeyConstraintError,
LockConflictError,
NotNullConstraintError,
UniqueConstraintError,
} from './database.error';
import {EntityNotFoundError} from './entity-not-found.error';

/**
* Normalizes or re-throws errors originating from legacy juggler or connector operations.
*/
export function handleRepositoryError(err: unknown): never {
if (!err) {
throw new DatabaseError('An unknown database execution error occurred.');
}

if (err instanceof DatabaseError) {
throw err;
}

if (typeof err === 'object' && err !== null && 'code' in err) {
const errorObj = err as {
code?: string;
message?: string;
details?: Record<string, unknown>;
};

switch (errorObj.code) {
case 'UNIQUE_CONSTRAINT_VIOLATION':
throw new UniqueConstraintError(errorObj.message, errorObj.details);
// returning entity not found error to avoid leaking db schema information
case 'TABLE_NOT_FOUND':
throw new EntityNotFoundError(
errorObj.message || 'record not found',
errorObj.details,
);
case 'FOREIGN_KEY_VIOLATION':
throw new ForeignKeyConstraintError(errorObj.message, errorObj.details);
case 'NOT_NULL_VIOLATION':
throw new NotNullConstraintError(errorObj.message, errorObj.details);
case 'CHECK_CONSTRAINT_VIOLATION':
throw new CheckConstraintError(errorObj.message, errorObj.details);
case 'DATA_TYPE_MISMATCH':
throw new DataTypeMismatchError(errorObj.message, errorObj.details);
case 'LOCK_CONFLICT':
throw new LockConflictError(errorObj.message, errorObj.details);
case 'CONNECTION_FAILURE':
throw new DatabaseConnectionError(errorObj.message, errorObj.details);
}
}

throw err;
}
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.error';
Loading
Loading