Fix delete reference lint errors

This commit is contained in:
Kari Lintulaakso
2025-11-17 17:51:14 +02:00
parent 95980a6db0
commit 0bec3de106
4 changed files with 205 additions and 11 deletions
@@ -52,6 +52,18 @@ describe('Deleting a reference', () => {
expect(body.length).toEqual(0) //There should be no authors linked to the id of the ledeted reference
})
it('fails when linked updates exist', async () => {
await login('testSu')
const { body, status } = await send<{ message: string }>('reference/10039', 'DELETE')
expect(status).toEqual(409)
expect(body).toEqual({ message: 'The Reference with associated updates cannot be deleted.' })
const { status: stillExistsStatus } = await send(`reference/10039`, 'GET')
expect(stillExistsStatus).toEqual(200)
})
it('without permissions fails', async () => {
logout()
const deleteResultNoPerm = await send('reference/10039', 'DELETE')
+41 -6
View File
@@ -1,3 +1,4 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
import { EditDataType, FixBigInt, ReferenceDetailsType } from '../../../../frontend/src/shared/types'
import { getFieldsOfTables, nowDb } from '../../utils/db'
import { getReferenceDetails } from '../reference'
@@ -6,6 +7,16 @@ import { writeReferenceAuthors } from './author'
import { writeReferenceJournal } from './journal'
import Prisma from '../../../prisma/generated/now_test_client'
const REFERENCE_HAS_UPDATES_ERROR_MESSAGE = 'The Reference with associated updates cannot be deleted.'
class ConflictError extends Error {
declare status: number
constructor(message: string) {
super(message)
this.status = 409
}
}
export const writeReference = async (reference: EditDataType<ReferenceDetailsType>) => {
const allowedColumns = getFieldsOfTables(['ref_ref', 'ref_authors', 'ref_journal'])
const filteredReference = filterAllowedKeys(reference, allowedColumns) as Prisma.ref_ref
@@ -61,15 +72,39 @@ export const deleteReference = async (rid: number) => {
if (!reference) throw new Error('Reference not found')
await nowDb.ref_authors.deleteMany({
where: { rid: rid },
})
try {
await nowDb.ref_ref.delete({
where: { rid: rid },
await nowDb.$transaction(async prisma => {
const linkedUpdateCounts = await Promise.all([
prisma.now_lr.count({ where: { rid } }),
prisma.now_sr.count({ where: { rid } }),
prisma.now_br.count({ where: { rid } }),
prisma.now_tr.count({ where: { rid } }),
prisma.now_tur.count({ where: { rid } }),
])
const hasLinkedUpdates = linkedUpdateCounts.some(count => count > 0)
if (hasLinkedUpdates) {
throw new ConflictError(REFERENCE_HAS_UPDATES_ERROR_MESSAGE)
}
await prisma.ref_authors.deleteMany({
where: { rid },
})
await prisma.ref_ref.delete({
where: { rid },
})
})
} catch (error) {
if (error instanceof ConflictError) {
throw error
}
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2003') {
throw new ConflictError(REFERENCE_HAS_UPDATES_ERROR_MESSAGE)
}
throw new Error('Failed to delete the reference')
}
}
@@ -16,6 +16,27 @@ import { useNotify } from '@/hooks/notification'
import { useEffect } from 'react'
import { createReferenceTitle } from './referenceFormatting'
import { useReturnNavigation } from '@/hooks/useReturnNavigation'
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'
import type { SerializedError } from '@reduxjs/toolkit'
const REFERENCE_DELETE_CONFLICT_MESSAGE = 'The Reference with associated updates cannot be deleted.'
const GENERIC_DELETE_MESSAGE = 'Could not delete item. Error happened.'
const resolveDeleteErrorMessage = (error: FetchBaseQueryError | SerializedError | undefined): string => {
if (error && 'status' in error) {
const errorData = (error.data ?? {}) as { message?: string }
if (typeof errorData.message === 'string' && errorData.message.trim().length > 0) {
return errorData.message
}
if (error.status === 409) {
return REFERENCE_DELETE_CONFLICT_MESSAGE
}
}
return GENERIC_DELETE_MESSAGE
}
export const ReferenceDetails = () => {
const { id } = useParams()
@@ -25,7 +46,7 @@ export const ReferenceDetails = () => {
}
const { isFetching, isError, data } = useGetReferenceDetailsQuery(id!, { skip: isNew })
const [editReferenceRequest, { isLoading: mutationLoading }] = useEditReferenceMutation()
const [deleteMutation, { isSuccess: deleteSuccess, isError: deleteError }] = useDeleteReferenceMutation()
const [deleteMutation, { isSuccess: deleteSuccess }] = useDeleteReferenceMutation()
const { notify } = useNotify()
const navigate = useNavigate()
const { fallbackTarget } = useReturnNavigation({ fallback: '/reference' })
@@ -34,10 +55,8 @@ export const ReferenceDetails = () => {
if (deleteSuccess) {
notify('Deleted item successfully.')
navigate(fallbackTarget)
} else if (deleteError) {
notify('Could not delete item. Error happened.', 'error')
}
}, [deleteSuccess, deleteError, notify, navigate, fallbackTarget])
}, [deleteSuccess, notify, navigate, fallbackTarget])
if (isError) return <div>Error loading data</div>
if (isFetching || (!data && !isNew) || mutationLoading) return <CircularProgress />
@@ -57,7 +76,12 @@ export const ReferenceDetails = () => {
}
const deleteFunction = async () => {
await deleteMutation(parseInt(id!)).unwrap()
try {
await deleteMutation(parseInt(id!)).unwrap()
} catch (error) {
const message = resolveDeleteErrorMessage(error as FetchBaseQueryError | SerializedError | undefined)
notify(message, 'error')
}
}
const tabs: TabType[] = [
@@ -0,0 +1,123 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
import { Notification, NotificationContextProvider } from '@/components/Notification'
import { ReferenceDetails } from '@/components/Reference/ReferenceDetails'
import {
useDeleteReferenceMutation,
useEditReferenceMutation,
useGetReferenceDetailsQuery,
} from '@/redux/referenceReducer'
import type { ReferenceDetailsType } from '@/shared/types'
jest.mock('@/components/DetailView/DetailView', () => ({
DetailView: ({ deleteFunction }: { deleteFunction?: () => Promise<void> }) => (
<button onClick={() => void deleteFunction?.()}>Delete</button>
),
}))
jest.mock('lodash-es', () => ({
cloneDeep: (value: unknown) => value,
}))
jest.mock('@/util/config', () => ({
BACKEND_URL: '',
ENABLE_WRITE: true,
ENV: 'test',
}))
jest.mock('@/redux/referenceReducer', () => ({
useGetReferenceDetailsQuery: jest.fn(),
useEditReferenceMutation: jest.fn(),
useDeleteReferenceMutation: jest.fn(),
}))
jest.mock('@/components/Reference/Tabs/ReferenceTab', () => ({ ReferenceTab: () => <div>Reference Tab</div> }))
jest.mock('@/components/Reference/Tabs/LocalityTab', () => ({ LocalityTab: () => <div>Locality Tab</div> }))
jest.mock('@/components/Reference/Tabs/SpeciesTab', () => ({ SpeciesTab: () => <div>Species Tab</div> }))
jest.mock('@/components/Reference/referenceFormatting', () => ({ createReferenceTitle: () => 'Reference title' }))
jest.mock('@/hooks/useReturnNavigation', () => ({ useReturnNavigation: () => ({ fallbackTarget: '/reference' }) }))
jest.mock('@/components/TimeBound/TimeBoundTable', () => ({ TimeBoundTable: () => <div>TimeBound Table</div> }))
jest.mock('@/components/Page', () => ({
usePageContext: () => ({
editRights: { delete: true, edit: true },
previousTableUrls: [],
setPreviousTableUrls: jest.fn(),
viewName: 'reference',
idList: [],
setIdList: jest.fn(),
idFieldName: 'rid',
createTitle: jest.fn(),
createSubtitle: jest.fn(),
sqlLimit: 25,
sqlOffset: 0,
sqlColumnFilters: [],
sqlOrderBy: [],
setSqlLimit: jest.fn(),
setSqlOffset: jest.fn(),
setSqlColumnFilters: jest.fn(),
setSqlOrderBy: jest.fn(),
}),
}))
const conflictError = Object.assign(new Error('The Reference with associated updates cannot be deleted.'), {
status: 409,
data: { message: 'The Reference with associated updates cannot be deleted.' },
})
describe('ReferenceDetails deletion', () => {
const originalConfirm = window.confirm
const mockDeleteTrigger = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
;(useGetReferenceDetailsQuery as jest.Mock).mockReturnValue({
isFetching: false,
isError: false,
data: {
rid: 1,
ref_authors: [],
ref_journal: { journal_id: 1, journal_title: '', short_title: '', alt_title: '', ISSN: '' },
ref_ref_type: { ref_type: '' },
exact_date: null,
} as unknown as ReferenceDetailsType,
})
;(useEditReferenceMutation as jest.Mock).mockReturnValue([
jest.fn(() => ({ unwrap: jest.fn() })),
{ isLoading: false },
])
mockDeleteTrigger.mockReturnValue({ unwrap: () => Promise.reject(conflictError) })
;(useDeleteReferenceMutation as jest.Mock).mockReturnValue([
mockDeleteTrigger,
{ isSuccess: false, error: conflictError },
])
window.confirm = jest.fn(() => true)
})
afterEach(() => {
window.confirm = originalConfirm
})
it('shows a conflict message when deletion fails due to linked updates', async () => {
const user = userEvent.setup()
render(
<NotificationContextProvider>
<Notification />
<MemoryRouter initialEntries={[{ pathname: '/reference/1' }]}>
<Routes>
<Route path="/reference/:id" element={<ReferenceDetails />} />
</Routes>
</MemoryRouter>
</NotificationContextProvider>
)
await user.click(await screen.findByRole('button', { name: /delete/i }))
await waitFor(() => {
expect(screen.getByText('The Reference with associated updates cannot be deleted.')).toBeTruthy()
})
})
})