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