modbus: add explicit cascade delete (device + readings + toggles + HA cleanup)
A disabled device with historical readings still cannot be deleted by default
(409 guard stays, to prevent accidental data loss). A new explicit opt-in lets
the user remove it for real:
- DELETE /api/modbus/devices/{uuid}?cascade=true removes the device's readings
and exposed_entity_toggle rows, then the device, in one transaction (FK order:
readings before device). Best-effort clears the HA discovery configs first
(MQTT not connected -> no-op, never blocks the delete). Default (no cascade)
unchanged. Returns 200 with deletion counts.
- Frontend: a 'Force Delete (all data)' button appears only after the 409, with
a red irreversible warning; cascade is sent only on that explicit action.
- Also corrected a stale comment: FK RESTRICT IS enforced at runtime now.
This commit is contained in:
@@ -173,7 +173,7 @@ describe('useUpdateDevice', () => {
|
||||
describe('useDeleteDevice', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('calls DELETE /api/modbus/devices/{uuid}', async () => {
|
||||
it('calls DELETE /api/modbus/devices/{uuid} without query param when cascade omitted', async () => {
|
||||
mockDelete.mockResolvedValue({ data: null })
|
||||
mockGet.mockResolvedValue({ data: { items: [], total: 0 } })
|
||||
|
||||
@@ -182,13 +182,32 @@ describe('useDeleteDevice', () => {
|
||||
const { result } = renderHook(() => useDeleteDevice(), { wrapper: Wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync('test-uuid-1')
|
||||
await result.current.mutateAsync({ uuid: 'test-uuid-1' })
|
||||
})
|
||||
|
||||
expect(mockDelete).toHaveBeenCalledWith('/api/modbus/devices/{uuid}', {
|
||||
params: { path: { uuid: 'test-uuid-1' } },
|
||||
})
|
||||
})
|
||||
|
||||
it('calls DELETE /api/modbus/devices/{uuid} with cascade=true in query when cascade=true', async () => {
|
||||
mockDelete.mockResolvedValue({
|
||||
data: { deleted: true, readings_deleted: 5, toggles_deleted: 2 },
|
||||
})
|
||||
mockGet.mockResolvedValue({ data: { items: [], total: 0 } })
|
||||
|
||||
const { Wrapper } = makeWrapper()
|
||||
const { useDeleteDevice } = await import('./hooks')
|
||||
const { result } = renderHook(() => useDeleteDevice(), { wrapper: Wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({ uuid: 'test-uuid-1', cascade: true })
|
||||
})
|
||||
|
||||
expect(mockDelete).toHaveBeenCalledWith('/api/modbus/devices/{uuid}', {
|
||||
params: { path: { uuid: 'test-uuid-1' }, query: { cascade: true } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useTestReadDevice', () => {
|
||||
|
||||
@@ -115,12 +115,21 @@ export function useUpdateDevice() {
|
||||
// Mutation: delete device
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface DeleteDeviceParams {
|
||||
uuid: string
|
||||
/** When true, perform a cascade delete (removes readings + expose toggles). */
|
||||
cascade?: boolean
|
||||
}
|
||||
|
||||
export function useDeleteDevice() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (uuid: string) =>
|
||||
mutationFn: ({ uuid, cascade }: DeleteDeviceParams) =>
|
||||
apiClient.DELETE('/api/modbus/devices/{uuid}', {
|
||||
params: { path: { uuid } },
|
||||
params: {
|
||||
path: { uuid },
|
||||
...(cascade ? { query: { cascade: true } } : {}),
|
||||
},
|
||||
}),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['modbus-devices'] }),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user