Handle time unit delete conflicts

This commit is contained in:
Kari Lintulaakso
2025-11-26 11:51:53 +02:00
parent 6b1fb7ee93
commit 15ee4dc863
4 changed files with 96 additions and 6 deletions
@@ -2,6 +2,7 @@ import { beforeAll, beforeEach, afterAll, describe, it, expect } from '@jest/glo
import { login, logout, send, resetDatabase, resetDatabaseTimeout } from '../utils'
import { pool } from '../../utils/db'
import { newTimeUnitBasis } from './data'
import { TIME_UNIT_IN_USE_MESSAGE } from '../../services/write/timeUnit'
describe('Deleting a time unit', () => {
beforeAll(async () => {
@@ -29,7 +30,8 @@ describe('Deleting a time unit', () => {
it('that is being used by another entity fails', async () => {
const deleteResult = await send(`time-unit/bahean`, 'DELETE') // used by Lantian-Shuijiazui locality
expect(deleteResult.status).toEqual(500)
expect(deleteResult.status).toEqual(409)
expect(deleteResult.body).toEqual({ message: TIME_UNIT_IN_USE_MESSAGE })
const getResult = await send(`time-unit/bahean`, 'GET')
expect(getResult.status).toEqual(200) // Time Bound response status was not 200 after failed deletion
})
+16 -3
View File
@@ -15,7 +15,7 @@ import {
validateEntireTimeUnit,
} from '../services/timeUnit'
import { getTimeBoundDetails, validateEntireTimeBound } from '../services/timeBound'
import { deleteTimeUnit, writeTimeUnit } from '../services/write/timeUnit'
import { ConflictError, deleteTimeUnit, writeTimeUnit } from '../services/write/timeUnit'
import { fixBigInt } from '../utils/common'
import { writeTimeBound } from '../services/write/timeBound'
@@ -118,8 +118,21 @@ router.put(
router.delete('/:id', requireOneOf([Role.Admin, Role.EditUnrestricted]), async (req, res) => {
const id = req.params.id
await deleteTimeUnit(id, req.user!)
res.status(200).send()
try {
await deleteTimeUnit(id, req.user!)
return res.status(200).send()
} catch (error) {
if (error instanceof ConflictError) {
return res.status(error.status).send({ message: error.message })
}
if (error instanceof Error && error.message === 'Time unit not found') {
return res.status(404).send({ message: error.message })
}
return res.status(500).send({ message: 'Failed to delete time unit' })
}
})
export default router
+47 -2
View File
@@ -7,12 +7,47 @@ import {
} from '../../../../frontend/src/shared/types'
import { NOW_DB_NAME } from '../../utils/config'
import { WriteHandler } from './writeOperations/writeHandler'
import { getFieldsOfTables } from '../../utils/db'
import { getFieldsOfTables, nowDb } from '../../utils/db'
import { ActionType } from './writeOperations/types'
import { getTimeUnitDetails } from '../timeUnit'
import { checkTimeUnitCascade } from '../../utils/cascadeHandler'
import { writeLocalityCascade } from './locality'
export class ConflictError extends Error {
declare status: number
constructor(message: string) {
super(message)
this.status = 409
}
}
export const TIME_UNIT_IN_USE_MESSAGE =
'Cannot delete time unit because it is referenced by other records (e.g., localities or time bounds).'
const FOREIGN_KEY_ERROR_CODES = ['ER_ROW_IS_REFERENCED', 'ER_ROW_IS_REFERENCED_2']
const isForeignKeyConstraintError = (error: unknown) => {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string' &&
FOREIGN_KEY_ERROR_CODES.includes((error as { code: string }).code)
)
}
const assertTimeUnitIsNotInUse = async (id: string) => {
const localityUsageCount = await nowDb.now_loc.count({
where: {
OR: [{ bfa_max: id }, { bfa_min: id }],
},
})
if (localityUsageCount > 0) {
throw new ConflictError(TIME_UNIT_IN_USE_MESSAGE)
}
}
const getTimeUnitWriteHandler = (type: ActionType) => {
return new WriteHandler({
dbName: NOW_DB_NAME,
@@ -76,13 +111,23 @@ export const writeTimeUnit = async (
export const deleteTimeUnit = async (id: string, user: User) => {
const timeUnit = await getTimeUnitDetails(id)
if (!timeUnit) throw new Error('Time unit not found')
await assertTimeUnitIsNotInUse(id)
const writeHandler = getTimeUnitWriteHandler('delete')
try {
await writeHandler.start()
await writeHandler.deleteObject('now_time_unit', timeUnit, ['tu_name'])
await writeHandler.logUpdatesAndComplete(user.initials)
} catch (e) {
await writeHandler.end()
if (writeHandler.connection) {
await writeHandler.end()
}
if (isForeignKeyConstraintError(e)) {
throw new ConflictError(TIME_UNIT_IN_USE_MESSAGE)
}
throw e
}
}
@@ -0,0 +1,30 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
import { deleteTimeUnit, ConflictError, TIME_UNIT_IN_USE_MESSAGE } from '../../services/write/timeUnit'
import { getTimeUnitDetails } from '../../services/timeUnit'
import { nowDb } from '../../utils/db'
import { User } from '../../../../frontend/src/shared/types'
jest.mock('../../services/timeUnit', () => ({
getTimeUnitDetails: jest.fn(),
}))
describe('deleteTimeUnit', () => {
const mockUser = { initials: 'TU' } as unknown as User
const mockedGetTimeUnitDetails = getTimeUnitDetails as jest.MockedFunction<typeof getTimeUnitDetails>
type TimeUnitDetailsResult = NonNullable<Awaited<ReturnType<typeof getTimeUnitDetails>>>
beforeEach(() => {
jest.restoreAllMocks()
jest.spyOn(nowDb.now_loc, 'count').mockResolvedValue(0)
mockedGetTimeUnitDetails.mockResolvedValue({ tu_name: 'test' } as unknown as TimeUnitDetailsResult)
})
it('throws a ConflictError when localities reference the time unit', async () => {
jest.spyOn(nowDb.now_loc, 'count').mockResolvedValueOnce(1)
const error = await deleteTimeUnit('bahean', mockUser).catch((thrownError: unknown) => thrownError)
expect(error).toBeInstanceOf(ConflictError)
expect((error as Error).message).toBe(TIME_UNIT_IN_USE_MESSAGE)
})
})