feat: Deep-Delete für Pairs und Bilder (Fragen/Statements/Audios/Objekte kaskadieren)

DELETE /pairs/:id räumt jetzt unreferenzierte Fragen/Statements samt
Audio-Dateien (DB + S3) mit auf. DELETE /pictures/:id löscht zusätzlich
die nur mit diesem Bild verknüpften Objekte inkl. deren Pairs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 21:25:15 +02:00
parent ddbd879dab
commit 25d1e89446
3 changed files with 86 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ const router = require('express').Router();
const { query } = require('../db');
const { fillMissingRow } = require('../lib/translate');
const { loadPairContext, computeReadiness, loadPairContent, translateWordGroup } = require('../lib/pairContent');
const { deletePairDeep } = require('../lib/deleteCascade');
const STATUSES = ['draft', 'reviewed', 'blocked', 'published'];
const ANSWER_TYPES = new Set(['yes_no', 'text', 'question', 'word']);
@@ -298,11 +299,11 @@ router.post('/:id/publish', async (req, res, next) => {
} catch (err) { next(err); }
});
// DELETE /api/pairs/:id
// DELETE /api/pairs/:id — Pair + (unreferenzierte) Frage/Statements + deren Audios (DB+S3)
router.delete('/:id', async (req, res, next) => {
try {
const result = await query('DELETE FROM pairs WHERE id = $1 RETURNING id', [req.params.id]);
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
const deleted = await deletePairDeep(req.params.id);
if (!deleted) return res.status(404).json({ error: 'Not found' });
res.status(204).end();
} catch (err) { next(err); }
});

View File

@@ -3,6 +3,7 @@ const multer = require('multer');
const { v4: uuidv4 } = require('uuid');
const { query } = require('../db');
const { uploadFile, deleteFile, keyFromUrl } = require('../s3');
const { deletePictureObjectsDeep } = require('../lib/deleteCascade');
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
@@ -153,12 +154,15 @@ router.patch('/:id', async (req, res, next) => {
} catch (err) { next(err); }
});
// DELETE /api/pictures/:id — Eintrag + Hetzner-Datei löschen
// DELETE /api/pictures/:id — Eintrag + Hetzner-Datei löschen,
// inkl. Objekte des Bildes und deren Pairs (Fragen/Statements/Audios kaskadieren)
router.delete('/:id', async (req, res, next) => {
try {
const existing = await query('SELECT picture_link FROM pictures WHERE id = $1', [req.params.id]);
if (!existing.rows.length) return res.status(404).json({ error: 'Not found' });
await deletePictureObjectsDeep(req.params.id);
const key = keyFromUrl(existing.rows[0].picture_link);
if (key) await deleteFile(key).catch(() => {});