mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Handle time unit delete conflicts
This commit is contained in:
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user