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');
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(`
CREATE TABLE IF NOT EXISTS pictures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -40,7 +52,7 @@ async function migrate() {
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
titel_de TEXT,
titel_en TEXT,
titel_se TEXT,
titel_sv TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'requested'
CHECK (status IN ('requested', 'translated', 'generated', 'blocked', 'published')),
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(),
titel_de TEXT,
titel_en TEXT,
titel_se TEXT,
titel_sv TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'requested'
CHECK (status IN ('requested', 'blocked', 'published')),
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
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'",
'difficulty_level SMALLINT', 'requested_at TIMESTAMPTZ',
'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ'];
@@ -123,7 +135,7 @@ async function migrate() {
CHECK (status IN ('draft', 'blocked', 'published')),
sentence_de TEXT,
sentence_en TEXT,
sentence_se TEXT,
sentence_sv TEXT,
blocked_topic TEXT,
published_at TIMESTAMPTZ,
blocked_at TIMESTAMPTZ,
@@ -134,7 +146,7 @@ async function migrate() {
const questionCols = [
"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',
];
for (const col of questionCols)
@@ -156,10 +168,10 @@ async function migrate() {
CHECK (status IN ('draft', 'blocked', 'published')),
negative_sentence_de TEXT,
negative_sentence_en TEXT,
negative_sentence_se TEXT,
negative_sentence_sv TEXT,
positive_sentence_de TEXT,
positive_sentence_en TEXT,
positive_sentence_se TEXT,
positive_sentence_sv TEXT,
blocked_topic TEXT,
published_at TIMESTAMPTZ,
blocked_at TIMESTAMPTZ,
@@ -170,8 +182,8 @@ async function migrate() {
const stmtCols = [
"status VARCHAR(20) NOT NULL DEFAULT 'draft'",
'negative_sentence_de TEXT', 'negative_sentence_en TEXT', 'negative_sentence_se TEXT',
'positive_sentence_de TEXT', 'positive_sentence_en TEXT', 'positive_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_sv TEXT',
'blocked_topic TEXT', 'published_at TIMESTAMPTZ', 'blocked_at TIMESTAMPTZ',
];
for (const col of stmtCols)
@@ -358,6 +370,62 @@ async function migrate() {
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');
}