Implement foreign key violation handling in EmployeesRepository delete methods

- Enhanced the `delete` and `bulkDelete` methods in `EmployeesRepository` to handle foreign key violations by throwing a `ConflictException` with a descriptive message.
- Added unit tests to verify that foreign key violations are correctly mapped to `ConflictException` and that unknown errors are rethrown as expected.
- Refactored error handling in the `delete` methods to improve clarity and maintainability.
This commit is contained in:
shancheas
2026-09-01 08:57:31 +07:00
parent 2955b974d2
commit 23028abd48
2 changed files with 57 additions and 11 deletions
@@ -220,6 +220,26 @@ describe('EmployeesRepository', () => {
); );
}); });
it('delete maps foreign-key violations to ConflictException', async () => {
returning.mockRejectedValueOnce({ code: '23503' });
await expect(repository.delete('emp-1')).rejects.toMatchObject({
constructor: ConflictException,
message: 'Employee is referenced by other records',
});
returning.mockRejectedValueOnce({
cause: { code: '23503' },
});
await expect(repository.delete('emp-1')).rejects.toBeInstanceOf(
ConflictException,
);
});
it('delete rethrows unknown errors', async () => {
returning.mockRejectedValueOnce(new Error('db down'));
await expect(repository.delete('emp-1')).rejects.toThrow('db down');
});
it('bulkUpdateStatus and bulkDelete return 0 for empty ids', async () => { it('bulkUpdateStatus and bulkDelete return 0 for empty ids', async () => {
await expect( await expect(
repository.bulkUpdateStatus([], Status.create('active'), 'user-1'), repository.bulkUpdateStatus([], Status.create('active'), 'user-1'),
@@ -240,6 +260,16 @@ describe('EmployeesRepository', () => {
await expect(repository.bulkDelete(['emp-1'])).resolves.toBe(1); await expect(repository.bulkDelete(['emp-1'])).resolves.toBe(1);
}); });
it('bulkDelete maps foreign-key violations to ConflictException', async () => {
returning.mockRejectedValueOnce({
cause: { code: '23503' },
});
await expect(repository.bulkDelete(['emp-1'])).rejects.toMatchObject({
constructor: ConflictException,
message: 'Employee is referenced by other records',
});
});
it('extendListQuery is a passthrough hook', () => { it('extendListQuery is a passthrough hook', () => {
const qb = { join: true }; const qb = { join: true };
expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb); expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb);
@@ -221,6 +221,7 @@ export class EmployeesRepository {
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
try {
const deleted = await this.db const deleted = await this.db
.delete(employees) .delete(employees)
.where(eq(employees.id, id)) .where(eq(employees.id, id))
@@ -228,17 +229,24 @@ export class EmployeesRepository {
if (deleted.length === 0) { if (deleted.length === 0) {
throw new NotFoundException('Employee not found'); throw new NotFoundException('Employee not found');
} }
} catch (error) {
this.rethrowForeignKeyViolation(error);
}
} }
async bulkDelete(ids: string[]): Promise<number> { async bulkDelete(ids: string[]): Promise<number> {
if (ids.length === 0) { if (ids.length === 0) {
return 0; return 0;
} }
try {
const deleted = await this.db const deleted = await this.db
.delete(employees) .delete(employees)
.where(inArray(employees.id, ids)) .where(inArray(employees.id, ids))
.returning({ id: employees.id }); .returning({ id: employees.id });
return deleted.length; return deleted.length;
} catch (error) {
this.rethrowForeignKeyViolation(error);
}
} }
private buildListWhere(filters: ListEmployeesFilters): SQL | undefined { private buildListWhere(filters: ListEmployeesFilters): SQL | undefined {
@@ -363,6 +371,14 @@ export class EmployeesRepository {
throw error; throw error;
} }
private rethrowForeignKeyViolation(error: unknown): never {
const err = this.unwrapDbError(error);
if (err.code === '23503') {
throw new ConflictException('Employee is referenced by other records');
}
throw error;
}
private unwrapDbError(error: unknown): { private unwrapDbError(error: unknown): {
code?: string; code?: string;
constraint?: string; constraint?: string;