Add missing relation endpoints for content_mentor migration

- GET /api/objects?picture_id=X filter via object_pictures join
- GET /api/objects/:id/words — full word details
- GET /api/objects/:id/pairs — pairs with nested question + statements
- GET /api/words?titel_de=X case-insensitive filter
- GET /api/pictures/:id/words — full word details
- DELETE /api/pictures/:id/words/:wordId — convenience unlink

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 15:07:18 +02:00
parent 9b0603427e
commit b4de0b98c3
3 changed files with 100 additions and 6 deletions

View File

@@ -28,10 +28,13 @@ async function getWithRelations(id) {
// GET /api/objects // GET /api/objects
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
const { status, limit = 50, offset = 0 } = req.query; const { status, picture_id, limit = 50, offset = 0 } = req.query;
const params = [Math.min(parseInt(limit), 500), parseInt(offset)]; const params = [Math.min(parseInt(limit), 500), parseInt(offset)];
const where = status ? `WHERE o.status = $3` : ''; const conditions = [];
if (status) params.push(status); if (status) { conditions.push(`o.status = $${params.length + 1}`); params.push(status); }
if (picture_id) { conditions.push(`op2.picture_id = $${params.length + 1}`); params.push(picture_id); }
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const join2 = picture_id ? `LEFT JOIN object_pictures op2 ON op2.object_id = o.id` : '';
const result = await query( const result = await query(
`SELECT o.*, `SELECT o.*,
COALESCE(json_agg(DISTINCT ow.word_id) FILTER (WHERE ow.word_id IS NOT NULL), '[]') AS word_ids, COALESCE(json_agg(DISTINCT ow.word_id) FILTER (WHERE ow.word_id IS NOT NULL), '[]') AS word_ids,
@@ -41,6 +44,7 @@ router.get('/', async (req, res, next) => {
LEFT JOIN object_words ow ON ow.object_id = o.id LEFT JOIN object_words ow ON ow.object_id = o.id
LEFT JOIN object_pictures op ON op.object_id = o.id LEFT JOIN object_pictures op ON op.object_id = o.id
LEFT JOIN object_pairs opr ON opr.object_id = o.id LEFT JOIN object_pairs opr ON opr.object_id = o.id
${join2}
${where} ${where}
GROUP BY o.id GROUP BY o.id
ORDER BY o.created_at DESC ORDER BY o.created_at DESC
@@ -110,6 +114,69 @@ router.delete('/:id', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// --- Relation detail reads ---
// GET /api/objects/:id/words — full word objects
router.get('/:id/words', async (req, res, next) => {
try {
const result = await query(
`SELECT w.* FROM words w
JOIN object_words ow ON ow.word_id = w.id
WHERE ow.object_id = $1
ORDER BY w.created_at DESC`,
[req.params.id]
);
res.json(result.rows);
} catch (err) { next(err); }
});
// GET /api/objects/:id/pairs — full pair objects with nested question + statements
router.get('/:id/pairs', async (req, res, next) => {
try {
const pairsResult = await query(
`SELECT p.* FROM pairs p
JOIN object_pairs op ON op.pair_id = p.id
WHERE op.object_id = $1
ORDER BY p.created_at DESC`,
[req.params.id]
);
const pairs = pairsResult.rows;
if (!pairs.length) return res.json([]);
// Fetch linked questions and statements in bulk
const qIds = [...new Set(pairs.map(p => p.question_id).filter(Boolean))];
const posIds = [...new Set(pairs.map(p => p.positive_statement_id).filter(Boolean))];
const negIds = [...new Set(pairs.map(p => p.negative_statement_id).filter(Boolean))];
const stmtIds = [...new Set([...posIds, ...negIds])];
const [qRows, stmtRows] = await Promise.all([
qIds.length ? query(`SELECT * FROM questions WHERE id = ANY($1)`, [qIds]) : { rows: [] },
stmtIds.length ? query(
`SELECT s.*,
COALESCE(json_agg(DISTINCT spw.word_id) FILTER (WHERE spw.word_id IS NOT NULL), '[]') AS positive_word_ids,
COALESCE(json_agg(DISTINCT snw.word_id) FILTER (WHERE snw.word_id IS NOT NULL), '[]') AS negative_word_ids
FROM statements s
LEFT JOIN statement_positive_words spw ON spw.statement_id = s.id
LEFT JOIN statement_negative_words snw ON snw.statement_id = s.id
WHERE s.id = ANY($1)
GROUP BY s.id`, [stmtIds]
) : { rows: [] },
]);
const qMap = Object.fromEntries(qRows.rows.map(r => [r.id, r]));
const stmtMap = Object.fromEntries(stmtRows.rows.map(r => [r.id, r]));
const enriched = pairs.map(p => ({
...p,
question: p.question_id ? qMap[p.question_id] || null : null,
positive_statement: p.positive_statement_id ? stmtMap[p.positive_statement_id] || null : null,
negative_statement: p.negative_statement_id ? stmtMap[p.negative_statement_id] || null : null,
}));
res.json(enriched);
} catch (err) { next(err); }
});
// --- Relations --- // --- Relations ---
// POST /api/objects/:id/words/:wordId // POST /api/objects/:id/words/:wordId

View File

@@ -33,6 +33,31 @@ router.get('/:id', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /api/pictures/:id/words — full word objects linked to this picture
router.get('/:id/words', async (req, res, next) => {
try {
const result = await query(
`SELECT w.* FROM words w
JOIN word_pictures wp ON wp.word_id = w.id
WHERE wp.picture_id = $1
ORDER BY w.created_at DESC`,
[req.params.id]
);
res.json(result.rows);
} catch (err) { next(err); }
});
// DELETE /api/pictures/:id/words/:wordId
router.delete('/:id/words/:wordId', async (req, res, next) => {
try {
await query(
`DELETE FROM word_pictures WHERE picture_id = $1 AND word_id = $2`,
[req.params.id, req.params.wordId]
);
res.status(204).end();
} catch (err) { next(err); }
});
// POST /api/pictures — neuen Eintrag anlegen // POST /api/pictures — neuen Eintrag anlegen
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {

View File

@@ -12,10 +12,12 @@ const STATUS_TIMESTAMP = {
// GET /api/words // GET /api/words
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
const { status, limit = 50, offset = 0 } = req.query; const { status, titel_de, limit = 50, offset = 0 } = req.query;
const params = [Math.min(parseInt(limit), 500), parseInt(offset)]; const params = [Math.min(parseInt(limit), 500), parseInt(offset)];
const where = status ? `WHERE w.status = $3` : ''; const conditions = [];
if (status) params.push(status); if (status) { conditions.push(`w.status = $${params.length + 1}`); params.push(status); }
if (titel_de) { conditions.push(`lower(w.titel_de) = lower($${params.length + 1})`); params.push(titel_de); }
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await query( const result = await query(
`SELECT w.*, `SELECT w.*,
COALESCE(json_agg(DISTINCT p.id) FILTER (WHERE p.id IS NOT NULL), '[]') AS picture_ids, COALESCE(json_agg(DISTINCT p.id) FILTER (WHERE p.id IS NOT NULL), '[]') AS picture_ids,