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