feat: words-Tabelle – Brysbaert-Import + hierarchische Kategorien + Batch-Anreicherung
- categories: parent_id (self-referential) + 49 Unterkategorien geseedet - words: neue Spalten conc_m, dom_pos, level, themenfeld_id + unique index titel_en - enrich_batches + word_generative Tabellen - src/lib/enrichWords.js: Batch-Anreicherung (DE/SV-Übersetzung, Wortart, CEFR, Themenfeld) - src/routes/wordGenerative.js: CRUD für KI-Bild-Pipeline - src/routes/words.js: Filter dom_pos/level/themenfeld_id/has_conc_m + picture_count - scripts/import-brysbaert.js: CSV-Import-Skript (lokal gegen Prod-DB) - POST /api/words/enrich-batch als manueller Trigger Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
69
src/routes/wordGenerative.js
Normal file
69
src/routes/wordGenerative.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const router = require('express').Router();
|
||||
const { query } = require('../db');
|
||||
|
||||
const STATUSES = ['pending', 'generating', 'generated', 'accepted', 'rejected'];
|
||||
|
||||
// GET /api/word-generative
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const { status, word_id, limit = 50, offset = 0 } = req.query;
|
||||
const params = [Math.min(parseInt(limit), 500), parseInt(offset)];
|
||||
const conditions = [];
|
||||
if (status) { conditions.push(`status = $${params.length + 1}`); params.push(status); }
|
||||
if (word_id) { conditions.push(`word_id = $${params.length + 1}`); params.push(word_id); }
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const result = await query(
|
||||
`SELECT * FROM word_generative ${where} ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
||||
params
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// POST /api/word-generative
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const { word_id, prompt, status } = req.body;
|
||||
if (!word_id) return res.status(400).json({ error: 'word_id ist erforderlich' });
|
||||
if (status && !STATUSES.includes(status))
|
||||
return res.status(400).json({ error: `status muss eines sein von: ${STATUSES.join(', ')}` });
|
||||
const result = await query(
|
||||
`INSERT INTO word_generative (word_id, prompt, status)
|
||||
VALUES ($1, $2, $3) RETURNING *`,
|
||||
[word_id, prompt || null, status || 'pending']
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// PATCH /api/word-generative/:id
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const allowed = ['prompt', 'status', 'picture_link'];
|
||||
const fields = Object.keys(req.body).filter(k => allowed.includes(k));
|
||||
if (!fields.length) return res.status(400).json({ error: 'Keine gültigen Felder angegeben' });
|
||||
if (req.body.status && !STATUSES.includes(req.body.status))
|
||||
return res.status(400).json({ error: `status muss eines sein von: ${STATUSES.join(', ')}` });
|
||||
const setClauses = fields.map((f, i) => `${f} = $${i + 1}`).join(', ');
|
||||
const values = [...fields.map(f => req.body[f]), req.params.id];
|
||||
const result = await query(
|
||||
`UPDATE word_generative SET ${setClauses} WHERE id = $${fields.length + 1} RETURNING *`,
|
||||
values
|
||||
);
|
||||
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// DELETE /api/word-generative/:id
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const result = await query(
|
||||
`DELETE FROM word_generative WHERE id = $1 RETURNING id`, [req.params.id]
|
||||
);
|
||||
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
|
||||
res.status(204).end();
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -12,11 +12,17 @@ const STATUS_TIMESTAMP = {
|
||||
// GET /api/words
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const { status, titel_de, search, limit = 50, offset = 0 } = req.query;
|
||||
const { status, titel_de, search, dom_pos, level, themenfeld_id, has_conc_m,
|
||||
limit = 50, offset = 0 } = req.query;
|
||||
const params = [Math.min(parseInt(limit), 500), parseInt(offset)];
|
||||
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); }
|
||||
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); }
|
||||
if (dom_pos) { conditions.push(`w.dom_pos = $${params.length + 1}`); params.push(dom_pos); }
|
||||
if (level) { conditions.push(`w.level = $${params.length + 1}`); params.push(level); }
|
||||
if (themenfeld_id) { conditions.push(`w.themenfeld_id = $${params.length + 1}`); params.push(themenfeld_id); }
|
||||
if (has_conc_m === 'true') conditions.push(`w.conc_m IS NOT NULL`);
|
||||
if (has_conc_m === 'false') conditions.push(`w.conc_m IS NULL`);
|
||||
if (search) {
|
||||
const p = `%${search.toLowerCase()}%`;
|
||||
conditions.push(`(lower(w.titel_de) LIKE $${params.length + 1} OR lower(w.titel_en) LIKE $${params.length + 1} OR lower(w.titel_sv) LIKE $${params.length + 1})`);
|
||||
@@ -26,12 +32,14 @@ router.get('/', async (req, res, next) => {
|
||||
const result = await query(
|
||||
`SELECT w.*,
|
||||
COALESCE(json_agg(DISTINCT p.id) FILTER (WHERE p.id IS NOT NULL), '[]') AS picture_ids,
|
||||
COALESCE(json_agg(DISTINCT c.id) FILTER (WHERE c.id IS NOT NULL), '[]') AS category_ids
|
||||
COALESCE(json_agg(DISTINCT c.id) FILTER (WHERE c.id IS NOT NULL), '[]') AS category_ids,
|
||||
COUNT(DISTINCT wp2.picture_id)::int AS picture_count
|
||||
FROM words w
|
||||
LEFT JOIN word_pictures wp ON wp.word_id = w.id
|
||||
LEFT JOIN pictures p ON p.id = wp.picture_id
|
||||
LEFT JOIN word_categories wc ON wc.word_id = w.id
|
||||
LEFT JOIN categories c ON c.id = wc.category_id
|
||||
LEFT JOIN word_pictures wp ON wp.word_id = w.id
|
||||
LEFT JOIN pictures p ON p.id = wp.picture_id
|
||||
LEFT JOIN word_categories wc ON wc.word_id = w.id
|
||||
LEFT JOIN categories c ON c.id = wc.category_id
|
||||
LEFT JOIN word_pictures wp2 ON wp2.word_id = w.id
|
||||
${where}
|
||||
GROUP BY w.id
|
||||
ORDER BY w.created_at DESC
|
||||
@@ -69,7 +77,8 @@ router.post('/', async (req, res, next) => {
|
||||
router.patch('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const allowed = ['titel_de', 'titel_en', 'titel_sv', 'status',
|
||||
'difficulty_level', 'requested_at', 'published_at', 'blocked_at'];
|
||||
'difficulty_level', 'requested_at', 'published_at', 'blocked_at',
|
||||
'conc_m', 'dom_pos', 'level', 'themenfeld_id'];
|
||||
const fields = Object.keys(req.body).filter(k => allowed.includes(k));
|
||||
if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' });
|
||||
|
||||
@@ -117,12 +126,14 @@ router.get('/:id', async (req, res, next) => {
|
||||
const result = await query(
|
||||
`SELECT w.*,
|
||||
COALESCE(json_agg(DISTINCT p.id) FILTER (WHERE p.id IS NOT NULL), '[]') AS picture_ids,
|
||||
COALESCE(json_agg(DISTINCT c.id) FILTER (WHERE c.id IS NOT NULL), '[]') AS category_ids
|
||||
COALESCE(json_agg(DISTINCT c.id) FILTER (WHERE c.id IS NOT NULL), '[]') AS category_ids,
|
||||
COUNT(DISTINCT wp2.picture_id)::int AS picture_count
|
||||
FROM words w
|
||||
LEFT JOIN word_pictures wp ON wp.word_id = w.id
|
||||
LEFT JOIN pictures p ON p.id = wp.picture_id
|
||||
LEFT JOIN word_categories wc ON wc.word_id = w.id
|
||||
LEFT JOIN categories c ON c.id = wc.category_id
|
||||
LEFT JOIN word_pictures wp ON wp.word_id = w.id
|
||||
LEFT JOIN pictures p ON p.id = wp.picture_id
|
||||
LEFT JOIN word_categories wc ON wc.word_id = w.id
|
||||
LEFT JOIN categories c ON c.id = wc.category_id
|
||||
LEFT JOIN word_pictures wp2 ON wp2.word_id = w.id
|
||||
WHERE w.id = $1
|
||||
GROUP BY w.id`,
|
||||
[req.params.id]
|
||||
|
||||
Reference in New Issue
Block a user