Add languages, user_names, users_public tables and routes; fix _se→_sv rename

- Fix broken rename migration array (sed had corrupted from values to _sv)
- Add languages table with status lifecycle and trilingual titles
- Add user_names table with unique lowercase index
- Add users_public table linking to user_names and languages (native/target)
- Wire all three new routes under /api/languages, /api/user-names, /api/users-public

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 13:47:52 +02:00
parent 217aab7dcd
commit 10570786e9
9 changed files with 332 additions and 29 deletions

View File

@@ -1,6 +1,18 @@
const { query } = require('./db'); const { query } = require('./db');
async function migrate() { async function migrate() {
// Rename _se → _sv (Swedish ISO 639-1 correction)
const renames = [
['words', 'titel_se', 'titel_sv'],
['categories', 'titel_se', 'titel_sv'],
['questions', 'sentence_se', 'sentence_sv'],
['statements', 'negative_sentence_se', 'negative_sentence_sv'],
['statements', 'positive_sentence_se', 'positive_sentence_sv'],
];
for (const [table, from, to] of renames) {
await query(`ALTER TABLE ${table} RENAME COLUMN ${from} TO ${to}`).catch(() => {});
}
await query(` await query(`
CREATE TABLE IF NOT EXISTS pictures ( CREATE TABLE IF NOT EXISTS pictures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -40,7 +52,7 @@ async function migrate() {
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
titel_de TEXT, titel_de TEXT,
titel_en TEXT, titel_en TEXT,
titel_se TEXT, titel_sv TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'requested' status VARCHAR(20) NOT NULL DEFAULT 'requested'
CHECK (status IN ('requested', 'translated', 'generated', 'blocked', 'published')), CHECK (status IN ('requested', 'translated', 'generated', 'blocked', 'published')),
difficulty_level SMALLINT CHECK (difficulty_level BETWEEN 1 AND 50), difficulty_level SMALLINT CHECK (difficulty_level BETWEEN 1 AND 50),
@@ -73,7 +85,7 @@ async function migrate() {
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
titel_de TEXT, titel_de TEXT,
titel_en TEXT, titel_en TEXT,
titel_se TEXT, titel_sv TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'requested' status VARCHAR(20) NOT NULL DEFAULT 'requested'
CHECK (status IN ('requested', 'blocked', 'published')), CHECK (status IN ('requested', 'blocked', 'published')),
difficulty_level SMALLINT CHECK (difficulty_level BETWEEN 1 AND 50), difficulty_level SMALLINT CHECK (difficulty_level BETWEEN 1 AND 50),
@@ -86,7 +98,7 @@ async function migrate() {
`); `);
// Felder nachrüsten falls Tabelle schon als Platzhalter existiert // Felder nachrüsten falls Tabelle schon als Platzhalter existiert
const catCols = ['titel_de TEXT', 'titel_en TEXT', 'titel_se TEXT', const catCols = ['titel_de TEXT', 'titel_en TEXT', 'titel_sv TEXT',
"status VARCHAR(20) NOT NULL DEFAULT 'requested'", "status VARCHAR(20) NOT NULL DEFAULT 'requested'",
'difficulty_level SMALLINT', 'requested_at TIMESTAMPTZ', 'difficulty_level SMALLINT', 'requested_at TIMESTAMPTZ',
'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ']; 'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ'];
@@ -123,7 +135,7 @@ async function migrate() {
CHECK (status IN ('draft', 'blocked', 'published')), CHECK (status IN ('draft', 'blocked', 'published')),
sentence_de TEXT, sentence_de TEXT,
sentence_en TEXT, sentence_en TEXT,
sentence_se TEXT, sentence_sv TEXT,
blocked_topic TEXT, blocked_topic TEXT,
published_at TIMESTAMPTZ, published_at TIMESTAMPTZ,
blocked_at TIMESTAMPTZ, blocked_at TIMESTAMPTZ,
@@ -134,7 +146,7 @@ async function migrate() {
const questionCols = [ const questionCols = [
"status VARCHAR(20) NOT NULL DEFAULT 'draft'", "status VARCHAR(20) NOT NULL DEFAULT 'draft'",
'sentence_de TEXT', 'sentence_en TEXT', 'sentence_se TEXT', 'sentence_de TEXT', 'sentence_en TEXT', 'sentence_sv TEXT',
'blocked_topic TEXT', 'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ', 'blocked_topic TEXT', 'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ',
]; ];
for (const col of questionCols) for (const col of questionCols)
@@ -156,10 +168,10 @@ async function migrate() {
CHECK (status IN ('draft', 'blocked', 'published')), CHECK (status IN ('draft', 'blocked', 'published')),
negative_sentence_de TEXT, negative_sentence_de TEXT,
negative_sentence_en TEXT, negative_sentence_en TEXT,
negative_sentence_se TEXT, negative_sentence_sv TEXT,
positive_sentence_de TEXT, positive_sentence_de TEXT,
positive_sentence_en TEXT, positive_sentence_en TEXT,
positive_sentence_se TEXT, positive_sentence_sv TEXT,
blocked_topic TEXT, blocked_topic TEXT,
published_at TIMESTAMPTZ, published_at TIMESTAMPTZ,
blocked_at TIMESTAMPTZ, blocked_at TIMESTAMPTZ,
@@ -170,8 +182,8 @@ async function migrate() {
const stmtCols = [ const stmtCols = [
"status VARCHAR(20) NOT NULL DEFAULT 'draft'", "status VARCHAR(20) NOT NULL DEFAULT 'draft'",
'negative_sentence_de TEXT', 'negative_sentence_en TEXT', 'negative_sentence_se TEXT', 'negative_sentence_de TEXT', 'negative_sentence_en TEXT', 'negative_sentence_sv TEXT',
'positive_sentence_de TEXT', 'positive_sentence_en TEXT', 'positive_sentence_se TEXT', 'positive_sentence_de TEXT', 'positive_sentence_en TEXT', 'positive_sentence_sv TEXT',
'blocked_topic TEXT', 'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ', 'blocked_topic TEXT', 'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ',
]; ];
for (const col of stmtCols) for (const col of stmtCols)
@@ -358,6 +370,62 @@ async function migrate() {
FOR EACH ROW EXECUTE FUNCTION update_updated_at() FOR EACH ROW EXECUTE FUNCTION update_updated_at()
`); `);
// languages
await query(`
CREATE TABLE IF NOT EXISTS languages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
titel_en TEXT,
titel_de TEXT,
titel_sv TEXT,
short_en VARCHAR(10),
status VARCHAR(20) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'blocked', 'published')),
published_at TIMESTAMPTZ,
blocked_at TIMESTAMPTZ,
blocked_topic TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await query(`
DROP TRIGGER IF EXISTS languages_updated_at ON languages;
CREATE TRIGGER languages_updated_at
BEFORE UPDATE ON languages
FOR EACH ROW EXECUTE FUNCTION update_updated_at()
`);
// user_names
await query(`
CREATE TABLE IF NOT EXISTS user_names (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username_lowercase TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await query(`CREATE UNIQUE INDEX IF NOT EXISTS user_names_lowercase_idx ON user_names (username_lowercase)`);
// users_public
await query(`
CREATE TABLE IF NOT EXISTS users_public (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username_id UUID REFERENCES user_names(id) ON DELETE SET NULL,
language_native_id UUID REFERENCES languages(id) ON DELETE SET NULL,
language_target_id UUID REFERENCES languages(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await query(`
DROP TRIGGER IF EXISTS users_public_updated_at ON users_public;
CREATE TRIGGER users_public_updated_at
BEFORE UPDATE ON users_public
FOR EACH ROW EXECUTE FUNCTION update_updated_at()
`);
console.log('Migration complete'); console.log('Migration complete');
} }

View File

@@ -35,6 +35,9 @@ app.use('/api/pairs', auth, require('./routes/pairs'));
app.use('/api/questions', auth, require('./routes/questions')); app.use('/api/questions', auth, require('./routes/questions'));
app.use('/api/statements', auth, require('./routes/statements')); app.use('/api/statements', auth, require('./routes/statements'));
app.use('/api/blocklist', auth, require('./routes/blocklist')); app.use('/api/blocklist', auth, require('./routes/blocklist'));
app.use('/api/languages', auth, require('./routes/languages'));
app.use('/api/user-names', auth, require('./routes/user-names'));
app.use('/api/users-public', auth, require('./routes/users-public'));
// 404 // 404
app.use((req, res) => { app.use((req, res) => {

View File

@@ -53,11 +53,11 @@ router.get('/:id', async (req, res, next) => {
// POST /api/categories // POST /api/categories
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {
const { titel_de, titel_en, titel_se, difficulty_level } = req.body; const { titel_de, titel_en, titel_sv, difficulty_level } = req.body;
const result = await query( const result = await query(
`INSERT INTO categories (titel_de, titel_en, titel_se, difficulty_level, requested_at) `INSERT INTO categories (titel_de, titel_en, titel_sv, difficulty_level, requested_at)
VALUES ($1, $2, $3, $4, NOW()) RETURNING *`, VALUES ($1, $2, $3, $4, NOW()) RETURNING *`,
[titel_de || null, titel_en || null, titel_se || null, difficulty_level || null] [titel_de || null, titel_en || null, titel_sv || null, difficulty_level || null]
); );
res.status(201).json({ ...result.rows[0], word_ids: [] }); res.status(201).json({ ...result.rows[0], word_ids: [] });
} catch (err) { next(err); } } catch (err) { next(err); }
@@ -66,7 +66,7 @@ router.post('/', async (req, res, next) => {
// PATCH /api/categories/:id // PATCH /api/categories/:id
router.patch('/:id', async (req, res, next) => { router.patch('/:id', async (req, res, next) => {
try { try {
const allowed = ['titel_de', 'titel_en', 'titel_se', 'status', 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'];
const fields = Object.keys(req.body).filter(k => allowed.includes(k)); const fields = Object.keys(req.body).filter(k => allowed.includes(k));
if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' }); if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' });

81
src/routes/languages.js Normal file
View File

@@ -0,0 +1,81 @@
const router = require('express').Router();
const { query } = require('../db');
const STATUSES = ['draft', 'blocked', 'published'];
const STATUS_TIMESTAMP = { published: 'published_at', blocked: 'blocked_at' };
// GET /api/languages
router.get('/', async (req, res, next) => {
try {
const { status, limit = 50, offset = 0 } = req.query;
const params = [Math.min(parseInt(limit), 500), parseInt(offset)];
const where = status ? `WHERE status = $3` : '';
if (status) params.push(status);
const result = await query(
`SELECT * FROM languages ${where} ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
params
);
res.json(result.rows);
} catch (err) { next(err); }
});
// GET /api/languages/:id
router.get('/:id', async (req, res, next) => {
try {
const result = await query('SELECT * FROM languages WHERE id = $1', [req.params.id]);
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
res.json(result.rows[0]);
} catch (err) { next(err); }
});
// POST /api/languages
router.post('/', async (req, res, next) => {
try {
const { titel_en, titel_de, titel_sv, short_en, blocked_topic } = req.body;
const result = await query(
`INSERT INTO languages (titel_en, titel_de, titel_sv, short_en, blocked_topic)
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
[titel_en || null, titel_de || null, titel_sv || null, short_en || null, blocked_topic || null]
);
res.status(201).json(result.rows[0]);
} catch (err) { next(err); }
});
// PATCH /api/languages/:id
router.patch('/:id', async (req, res, next) => {
try {
const allowed = ['titel_en', 'titel_de', 'titel_sv', 'short_en', 'status',
'published_at', 'blocked_at', 'blocked_topic'];
const fields = Object.keys(req.body).filter(k => allowed.includes(k));
if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' });
if (req.body.status && !STATUSES.includes(req.body.status))
return res.status(400).json({ error: `status must be one of: ${STATUSES.join(', ')}` });
const tsField = STATUS_TIMESTAMP[req.body.status];
if (tsField && !req.body[tsField]) {
fields.push(tsField);
req.body[tsField] = new Date().toISOString();
}
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 languages 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/languages/:id
router.delete('/:id', async (req, res, next) => {
try {
const result = await query('DELETE FROM languages 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;

View File

@@ -31,11 +31,11 @@ router.get('/:id', async (req, res, next) => {
// POST /api/questions // POST /api/questions
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {
const { sentence_de, sentence_en, sentence_se, blocked_topic } = req.body; const { sentence_de, sentence_en, sentence_sv, blocked_topic } = req.body;
const result = await query( const result = await query(
`INSERT INTO questions (sentence_de, sentence_en, sentence_se, blocked_topic) `INSERT INTO questions (sentence_de, sentence_en, sentence_sv, blocked_topic)
VALUES ($1, $2, $3, $4) RETURNING *`, VALUES ($1, $2, $3, $4) RETURNING *`,
[sentence_de || null, sentence_en || null, sentence_se || null, blocked_topic || null] [sentence_de || null, sentence_en || null, sentence_sv || null, blocked_topic || null]
); );
res.status(201).json(result.rows[0]); res.status(201).json(result.rows[0]);
} catch (err) { next(err); } } catch (err) { next(err); }
@@ -44,7 +44,7 @@ router.post('/', async (req, res, next) => {
// PATCH /api/questions/:id // PATCH /api/questions/:id
router.patch('/:id', async (req, res, next) => { router.patch('/:id', async (req, res, next) => {
try { try {
const allowed = ['status', 'sentence_de', 'sentence_en', 'sentence_se', const allowed = ['status', 'sentence_de', 'sentence_en', 'sentence_sv',
'blocked_topic', 'published_at', 'blocked_at']; 'blocked_topic', 'published_at', 'blocked_at'];
const fields = Object.keys(req.body).filter(k => allowed.includes(k)); const fields = Object.keys(req.body).filter(k => allowed.includes(k));
if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' }); if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' });

View File

@@ -56,17 +56,17 @@ router.get('/:id', async (req, res, next) => {
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {
const { const {
negative_sentence_de, negative_sentence_en, negative_sentence_se, negative_sentence_de, negative_sentence_en, negative_sentence_sv,
positive_sentence_de, positive_sentence_en, positive_sentence_se, positive_sentence_de, positive_sentence_en, positive_sentence_sv,
blocked_topic, blocked_topic,
} = req.body; } = req.body;
const result = await query( const result = await query(
`INSERT INTO statements `INSERT INTO statements
(negative_sentence_de, negative_sentence_en, negative_sentence_se, (negative_sentence_de, negative_sentence_en, negative_sentence_sv,
positive_sentence_de, positive_sentence_en, positive_sentence_se, blocked_topic) positive_sentence_de, positive_sentence_en, positive_sentence_sv, blocked_topic)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`, VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
[negative_sentence_de || null, negative_sentence_en || null, negative_sentence_se || null, [negative_sentence_de || null, negative_sentence_en || null, negative_sentence_sv || null,
positive_sentence_de || null, positive_sentence_en || null, positive_sentence_se || null, positive_sentence_de || null, positive_sentence_en || null, positive_sentence_sv || null,
blocked_topic || null] blocked_topic || null]
); );
res.status(201).json({ ...result.rows[0], positive_word_ids: [], negative_word_ids: [] }); res.status(201).json({ ...result.rows[0], positive_word_ids: [], negative_word_ids: [] });
@@ -77,8 +77,8 @@ router.post('/', async (req, res, next) => {
router.patch('/:id', async (req, res, next) => { router.patch('/:id', async (req, res, next) => {
try { try {
const allowed = ['status', 'blocked_topic', 'published_at', 'blocked_at', const allowed = ['status', 'blocked_topic', 'published_at', 'blocked_at',
'negative_sentence_de', 'negative_sentence_en', 'negative_sentence_se', 'negative_sentence_de', 'negative_sentence_en', 'negative_sentence_sv',
'positive_sentence_de', 'positive_sentence_en', 'positive_sentence_se']; 'positive_sentence_de', 'positive_sentence_en', 'positive_sentence_sv'];
const fields = Object.keys(req.body).filter(k => allowed.includes(k)); const fields = Object.keys(req.body).filter(k => allowed.includes(k));
if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' }); if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' });

67
src/routes/user-names.js Normal file
View File

@@ -0,0 +1,67 @@
const router = require('express').Router();
const { query } = require('../db');
// GET /api/user-names
router.get('/', async (req, res, next) => {
try {
const { limit = 50, offset = 0 } = req.query;
const result = await query(
`SELECT * FROM user_names ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
[Math.min(parseInt(limit), 500), parseInt(offset)]
);
res.json(result.rows);
} catch (err) { next(err); }
});
// GET /api/user-names/:id
router.get('/:id', async (req, res, next) => {
try {
const result = await query('SELECT * FROM user_names WHERE id = $1', [req.params.id]);
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
res.json(result.rows[0]);
} catch (err) { next(err); }
});
// POST /api/user-names
router.post('/', async (req, res, next) => {
try {
const { username } = req.body;
if (!username) return res.status(400).json({ error: 'username is required' });
const result = await query(
`INSERT INTO user_names (username, username_lowercase) VALUES ($1, $2) RETURNING *`,
[username, username.toLowerCase()]
);
res.status(201).json(result.rows[0]);
} catch (err) {
if (err.code === '23505') return res.status(409).json({ error: 'Username already taken' });
next(err);
}
});
// PATCH /api/user-names/:id
router.patch('/:id', async (req, res, next) => {
try {
const { username } = req.body;
if (!username) return res.status(400).json({ error: 'No valid fields provided' });
const result = await query(
`UPDATE user_names SET username = $1, username_lowercase = $2 WHERE id = $3 RETURNING *`,
[username, username.toLowerCase(), req.params.id]
);
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
res.json(result.rows[0]);
} catch (err) {
if (err.code === '23505') return res.status(409).json({ error: 'Username already taken' });
next(err);
}
});
// DELETE /api/user-names/:id
router.delete('/:id', async (req, res, next) => {
try {
const result = await query('DELETE FROM user_names 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;

View File

@@ -0,0 +1,84 @@
const router = require('express').Router();
const { query } = require('../db');
// GET /api/users-public
router.get('/', async (req, res, next) => {
try {
const { limit = 50, offset = 0 } = req.query;
const result = await query(
`SELECT up.*,
un.username, un.username_lowercase,
ln.short_en AS language_native_short, ln.titel_en AS language_native_en,
lt.short_en AS language_target_short, lt.titel_en AS language_target_en
FROM users_public up
LEFT JOIN user_names un ON un.id = up.username_id
LEFT JOIN languages ln ON ln.id = up.language_native_id
LEFT JOIN languages lt ON lt.id = up.language_target_id
ORDER BY up.created_at DESC LIMIT $1 OFFSET $2`,
[Math.min(parseInt(limit), 500), parseInt(offset)]
);
res.json(result.rows);
} catch (err) { next(err); }
});
// GET /api/users-public/:id
router.get('/:id', async (req, res, next) => {
try {
const result = await query(
`SELECT up.*,
un.username, un.username_lowercase,
ln.short_en AS language_native_short, ln.titel_en AS language_native_en,
lt.short_en AS language_target_short, lt.titel_en AS language_target_en
FROM users_public up
LEFT JOIN user_names un ON un.id = up.username_id
LEFT JOIN languages ln ON ln.id = up.language_native_id
LEFT JOIN languages lt ON lt.id = up.language_target_id
WHERE up.id = $1`,
[req.params.id]
);
if (!result.rows.length) return res.status(404).json({ error: 'Not found' });
res.json(result.rows[0]);
} catch (err) { next(err); }
});
// POST /api/users-public
router.post('/', async (req, res, next) => {
try {
const { username_id, language_native_id, language_target_id } = req.body;
const result = await query(
`INSERT INTO users_public (username_id, language_native_id, language_target_id)
VALUES ($1, $2, $3) RETURNING *`,
[username_id || null, language_native_id || null, language_target_id || null]
);
res.status(201).json(result.rows[0]);
} catch (err) { next(err); }
});
// PATCH /api/users-public/:id
router.patch('/:id', async (req, res, next) => {
try {
const allowed = ['username_id', 'language_native_id', 'language_target_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' });
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 users_public 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/users-public/:id
router.delete('/:id', async (req, res, next) => {
try {
const result = await query('DELETE FROM users_public 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;

View File

@@ -59,11 +59,11 @@ router.get('/:id', async (req, res, next) => {
// POST /api/words // POST /api/words
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {
const { titel_de, titel_en, titel_se, difficulty_level } = req.body; const { titel_de, titel_en, titel_sv, difficulty_level } = req.body;
const result = await query( const result = await query(
`INSERT INTO words (titel_de, titel_en, titel_se, difficulty_level, requested_at) `INSERT INTO words (titel_de, titel_en, titel_sv, difficulty_level, requested_at)
VALUES ($1, $2, $3, $4, NOW()) RETURNING *`, VALUES ($1, $2, $3, $4, NOW()) RETURNING *`,
[titel_de || null, titel_en || null, titel_se || null, difficulty_level || null] [titel_de || null, titel_en || null, titel_sv || null, difficulty_level || null]
); );
res.status(201).json({ ...result.rows[0], picture_ids: [], category_ids: [] }); res.status(201).json({ ...result.rows[0], picture_ids: [], category_ids: [] });
} catch (err) { next(err); } } catch (err) { next(err); }
@@ -72,7 +72,7 @@ router.post('/', async (req, res, next) => {
// PATCH /api/words/:id // PATCH /api/words/:id
router.patch('/:id', async (req, res, next) => { router.patch('/:id', async (req, res, next) => {
try { try {
const allowed = ['titel_de', 'titel_en', 'titel_se', 'status', 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'];
const fields = Object.keys(req.body).filter(k => allowed.includes(k)); const fields = Object.keys(req.body).filter(k => allowed.includes(k));
if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' }); if (!fields.length) return res.status(400).json({ error: 'No valid fields provided' });