From b4de0b98c3897041052c86f803ff924e6bbff77f Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 21 May 2026 15:07:18 +0200 Subject: [PATCH] Add missing relation endpoints for content_mentor migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/routes/objects.js | 73 ++++++++++++++++++++++++++++++++++++++++-- src/routes/pictures.js | 25 +++++++++++++++ src/routes/words.js | 8 +++-- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/routes/objects.js b/src/routes/objects.js index a85b2a6..7ea3426 100644 --- a/src/routes/objects.js +++ b/src/routes/objects.js @@ -28,10 +28,13 @@ async function getWithRelations(id) { // GET /api/objects router.get('/', async (req, res, next) => { 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 where = status ? `WHERE o.status = $3` : ''; - if (status) params.push(status); + const conditions = []; + 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( `SELECT o.*, 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_pictures op ON op.object_id = o.id LEFT JOIN object_pairs opr ON opr.object_id = o.id + ${join2} ${where} GROUP BY o.id ORDER BY o.created_at DESC @@ -110,6 +114,69 @@ router.delete('/:id', async (req, res, next) => { } 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 --- // POST /api/objects/:id/words/:wordId diff --git a/src/routes/pictures.js b/src/routes/pictures.js index 89bd9b3..5c34710 100644 --- a/src/routes/pictures.js +++ b/src/routes/pictures.js @@ -33,6 +33,31 @@ router.get('/:id', async (req, res, next) => { } 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 router.post('/', async (req, res, next) => { try { diff --git a/src/routes/words.js b/src/routes/words.js index 638c666..03160d4 100644 --- a/src/routes/words.js +++ b/src/routes/words.js @@ -12,10 +12,12 @@ const STATUS_TIMESTAMP = { // GET /api/words router.get('/', async (req, res, next) => { 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 where = status ? `WHERE w.status = $3` : ''; - if (status) params.push(status); + const conditions = []; + 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( `SELECT w.*, COALESCE(json_agg(DISTINCT p.id) FILTER (WHERE p.id IS NOT NULL), '[]') AS picture_ids,