Handle duplicate time unit creation conflicts

This commit is contained in:
Kari Lintulaakso
2025-11-26 14:41:40 +02:00
parent ba2557f340
commit 37b67f0de1
3 changed files with 130 additions and 57 deletions
@@ -58,6 +58,18 @@ describe('Creating new time unit', () => {
})
})
it('Creation fails with duplicate display name and returns structured error', async () => {
const duplicateResult = await send('time-unit', 'PUT', {
timeUnit: { ...newTimeUnitBasis },
})
expect(duplicateResult.status).toEqual(409)
expect(duplicateResult.body).toEqual({
message: 'Time unit with the provided name already exists',
code: 'duplicate_name',
})
})
it('Creation fails without permissions', async () => {
logout()
const resultNoPerm = await send('time-unit', 'PUT', {
+64 -56
View File
@@ -15,7 +15,7 @@ import {
validateEntireTimeUnit,
} from '../services/timeUnit'
import { getTimeBoundDetails, validateEntireTimeBound } from '../services/timeBound'
import { ConflictError, deleteTimeUnit, writeTimeUnit } from '../services/write/timeUnit'
import { ConflictError, DuplicateTimeUnitError, deleteTimeUnit, writeTimeUnit } from '../services/write/timeUnit'
import { fixBigInt } from '../utils/common'
import { writeTimeBound } from '../services/write/timeBound'
@@ -45,74 +45,82 @@ router.put(
'/',
requireOneOf([Role.Admin, Role.EditUnrestricted]),
async (req: Request<object, object, { timeUnit: EditDataType<TimeUnitDetailsType> & EditMetaData }>, res) => {
const { comment, references, ...editedTimeUnit } = req.body.timeUnit
try {
const { comment, references, ...editedTimeUnit } = req.body.timeUnit
let validationUpBound = undefined
let validationLowBound = undefined
let newUpBoundId = undefined
let newLowBoundId = undefined
let validationUpBound = undefined
let validationLowBound = undefined
let newUpBoundId = undefined
let newLowBoundId = undefined
// new up bound has been created in time unit edit view
if (!editedTimeUnit.up_bnd && editedTimeUnit.up_bound) {
const newUpBound = {
...editedTimeUnit.up_bound,
now_bau: [],
references: references,
} as EditDataType<TimeBoundDetailsType> & EditMetaData
// new up bound has been created in time unit edit view
if (!editedTimeUnit.up_bnd && editedTimeUnit.up_bound) {
const newUpBound = {
...editedTimeUnit.up_bound,
now_bau: [],
references: references,
} as EditDataType<TimeBoundDetailsType> & EditMetaData
const newUpBoundValidationErrors = await validateEntireTimeBound(newUpBound)
if (newUpBoundValidationErrors.length > 0) {
return res.status(403).send(newUpBoundValidationErrors)
const newUpBoundValidationErrors = await validateEntireTimeBound(newUpBound)
if (newUpBoundValidationErrors.length > 0) {
return res.status(403).send(newUpBoundValidationErrors)
}
const { result } = await writeTimeBound(newUpBound, undefined, references, req.user!.initials)
newUpBoundId = result
validationUpBound = (await getTimeBoundDetails(newUpBoundId)) ?? undefined
} else if (editedTimeUnit.up_bnd) {
validationUpBound = (await getTimeBoundDetails(editedTimeUnit.up_bnd)) ?? undefined
}
const { result } = await writeTimeBound(newUpBound, undefined, references, req.user!.initials)
newUpBoundId = result
validationUpBound = (await getTimeBoundDetails(newUpBoundId)) ?? undefined
} else if (editedTimeUnit.up_bnd) {
validationUpBound = (await getTimeBoundDetails(editedTimeUnit.up_bnd)) ?? undefined
}
// new low bound has been created in time unit edit view
if (!editedTimeUnit.low_bnd && editedTimeUnit.low_bound) {
const newLowBound = { ...editedTimeUnit.low_bound, now_bau: [], references: references }
// new low bound has been created in time unit edit view
if (!editedTimeUnit.low_bnd && editedTimeUnit.low_bound) {
const newLowBound = { ...editedTimeUnit.low_bound, now_bau: [], references: references }
const newLowBoundValidationErrors = await validateEntireTimeBound(newLowBound)
if (newLowBoundValidationErrors.length > 0) {
return res.status(403).send(newLowBoundValidationErrors)
}
const newLowBoundValidationErrors = await validateEntireTimeBound(newLowBound)
if (newLowBoundValidationErrors.length > 0) {
return res.status(403).send(newLowBoundValidationErrors)
const { result } = await writeTimeBound(newLowBound, undefined, references, req.user!.initials)
newLowBoundId = result
validationLowBound = (await getTimeBoundDetails(newLowBoundId)) ?? undefined
} else if (editedTimeUnit.low_bnd) {
validationLowBound = (await getTimeBoundDetails(editedTimeUnit.low_bnd)) ?? undefined
}
const { result } = await writeTimeBound(newLowBound, undefined, references, req.user!.initials)
newLowBoundId = result
validationLowBound = (await getTimeBoundDetails(newLowBoundId)) ?? undefined
} else if (editedTimeUnit.low_bnd) {
validationLowBound = (await getTimeBoundDetails(editedTimeUnit.low_bnd)) ?? undefined
}
const validationErrors = await validateEntireTimeUnit({
...editedTimeUnit,
up_bnd: newUpBoundId ?? editedTimeUnit.up_bnd,
up_bound: validationUpBound,
low_bnd: newLowBoundId ?? editedTimeUnit.low_bnd,
low_bound: validationLowBound,
references: references,
})
if (validationErrors.length > 0) {
return res.status(403).send(validationErrors)
}
const { tu_name, errorObject } = await writeTimeUnit(
{
const validationErrors = await validateEntireTimeUnit({
...editedTimeUnit,
up_bnd: newUpBoundId ?? editedTimeUnit.up_bnd,
up_bound: validationUpBound,
low_bnd: newLowBoundId ?? editedTimeUnit.low_bnd,
},
comment,
references,
req.user!.initials
)
if (errorObject) {
return res.status(403).send(errorObject)
low_bound: validationLowBound,
references: references,
})
if (validationErrors.length > 0) {
return res.status(403).send(validationErrors)
}
const { tu_name, errorObject } = await writeTimeUnit(
{
...editedTimeUnit,
up_bnd: newUpBoundId ?? editedTimeUnit.up_bnd,
low_bnd: newLowBoundId ?? editedTimeUnit.low_bnd,
},
comment,
references,
req.user!.initials
)
if (errorObject) {
return res.status(403).send(errorObject)
}
return res.status(200).send({ tu_name })
} catch (error) {
if (error instanceof DuplicateTimeUnitError) {
return res.status(error.status).send({ message: error.message, code: error.code })
}
return res.status(500).send({ message: 'Failed to write time unit' })
}
return res.status(200).send({ tu_name })
}
)
+54 -1
View File
@@ -36,6 +36,30 @@ const isForeignKeyConstraintError = (error: unknown) => {
)
}
const DUPLICATE_ENTRY_ERROR_CODE = 'ER_DUP_ENTRY'
const DUPLICATE_TIME_UNIT_MESSAGE = 'Time unit with the provided name already exists'
export class DuplicateTimeUnitError extends Error {
declare status: number
code: 'duplicate_name'
constructor(message: string) {
super(message)
this.status = 409
this.code = 'duplicate_name'
}
}
const isDuplicateEntryError = (error: unknown) => {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
typeof (error as { code: unknown }).code === 'string' &&
(error as { code: string }).code === DUPLICATE_ENTRY_ERROR_CODE
)
}
const assertTimeUnitIsNotInUse = async (id: string) => {
const localityUsageCount = await nowDb.now_loc.count({
where: {
@@ -48,6 +72,20 @@ const assertTimeUnitIsNotInUse = async (id: string) => {
}
}
const assertTimeUnitNameIsUnique = async (displayName: string, existingId?: string) => {
const normalizedId = createTimeUnitId(displayName)
const existingTimeUnit = await nowDb.now_time_unit.findFirst({
where: {
OR: [{ tu_name: normalizedId }, { tu_display_name: displayName }],
},
select: { tu_name: true },
})
if (existingTimeUnit && (!existingId || existingTimeUnit.tu_name !== existingId)) {
throw new DuplicateTimeUnitError(DUPLICATE_TIME_UNIT_MESSAGE)
}
}
const getTimeUnitWriteHandler = (type: ActionType) => {
return new WriteHandler({
dbName: NOW_DB_NAME,
@@ -70,6 +108,10 @@ export const writeTimeUnit = async (
const createdId = createTimeUnitId(timeUnit.tu_display_name!)
if (!timeUnit.tu_name) {
await assertTimeUnitNameIsUnique(timeUnit.tu_display_name!)
}
try {
await writeHandler.start()
@@ -103,7 +145,18 @@ export const writeTimeUnit = async (
return { tu_name: timeUnit.tu_name, errorObject: undefined }
} catch (e) {
await writeHandler.end()
if (writeHandler.connection) {
await writeHandler.end()
}
if (e instanceof DuplicateTimeUnitError) {
throw e
}
if (isDuplicateEntryError(e)) {
throw new DuplicateTimeUnitError(DUPLICATE_TIME_UNIT_MESSAGE)
}
throw e
}
}