fix: prompt_styles.themenfeld_id → kategorie_id mit FK auf categories

- Spalte umbenannt (idempotent ALTER TABLE)
- FK-Constraint zu categories hinzugefügt
- Seed befüllt kategorie_id per Kategoriename-Lookup (unabhängig von UUIDs)
- Route prompt-styles.js angepasst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tim Leikauf
2026-06-20 21:15:28 +02:00
parent dbd077e239
commit 1085a54761
2 changed files with 86 additions and 49 deletions

View File

@@ -999,60 +999,97 @@ async function migratePlaceholders() {
async function migratePromptStyles() { async function migratePromptStyles() {
await query(` await query(`
CREATE TABLE IF NOT EXISTS prompt_styles ( CREATE TABLE IF NOT EXISTS prompt_styles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(20) NOT NULL CHECK (type IN ('fix', 'atmosphere', 'setting')), type VARCHAR(20) NOT NULL CHECK (type IN ('fix', 'atmosphere', 'setting')),
themenfeld_id UUID, kategorie_id UUID,
text_en TEXT NOT NULL text_en TEXT NOT NULL
) )
`); `);
// Seed-Daten aus prompt_styles.csv (idempotent per id) // Umbenennung themenfeld_id → kategorie_id (idempotent)
await query(`ALTER TABLE prompt_styles RENAME COLUMN themenfeld_id TO kategorie_id`).catch(() => {});
// FK auf categories nachrüsten (idempotent)
await query(`
ALTER TABLE prompt_styles
ADD CONSTRAINT prompt_styles_kategorie_fk
FOREIGN KEY (kategorie_id) REFERENCES categories(id) ON DELETE SET NULL
`).catch(() => {});
// Seed-Daten aus prompt_styles.csv (idempotent per id, kategorie_id zunächst null)
const seeds = [ const seeds = [
{ id: 'b0f5c2a4-a95d-426f-a01c-0edc53e719b8', type: 'fix', themenfeld_id: null, text_en: 'hyperrealistic photography, natural unposed moment, shot on Canon EOS R5, ambient natural light, no color grading, razor sharp details, photorealistic textures, each object clearly visible and spatially separated, 8k' }, { id: 'b0f5c2a4-a95d-426f-a01c-0edc53e719b8', type: 'fix', text_en: 'hyperrealistic photography, natural unposed moment, shot on Canon EOS R5, ambient natural light, no color grading, razor sharp details, photorealistic textures, each object clearly visible and spatially separated, 8k' },
{ id: '62015070-1fbe-40b8-b293-8c39ae5994c3', type: 'atmosphere', themenfeld_id: null, text_en: 'misty autumn morning, golden hour light breaking through cool gray clouds, frost on the ground, dew on surfaces' }, { id: '62015070-1fbe-40b8-b293-8c39ae5994c3', type: 'atmosphere', text_en: 'misty autumn morning, golden hour light breaking through cool gray clouds, frost on the ground, dew on surfaces' },
{ id: 'd644f215-25b9-49be-87ea-629d7d8acb78', type: 'atmosphere', themenfeld_id: null, text_en: 'bright summer midday, harsh direct sunlight, vivid colors, dry warm air' }, { id: 'd644f215-25b9-49be-87ea-629d7d8acb78', type: 'atmosphere', text_en: 'bright summer midday, harsh direct sunlight, vivid colors, dry warm air' },
{ id: 'da0a5339-37f5-47be-ba63-1fbf6c1e9f90', type: 'atmosphere', themenfeld_id: null, text_en: 'overcast spring day, soft diffused light, fresh green tones, slightly cool atmosphere' }, { id: 'da0a5339-37f5-47be-ba63-1fbf6c1e9f90', type: 'atmosphere', text_en: 'overcast spring day, soft diffused light, fresh green tones, slightly cool atmosphere' },
{ id: '11a8edb4-90a3-48a8-8407-31056644b55a', type: 'atmosphere', themenfeld_id: null, text_en: 'golden winter afternoon, low sun casting long shadows, bare trees, cold crisp air' }, { id: '11a8edb4-90a3-48a8-8407-31056644b55a', type: 'atmosphere', text_en: 'golden winter afternoon, low sun casting long shadows, bare trees, cold crisp air' },
{ id: '97bad727-6555-4f48-9a68-17dd5ce85535', type: 'atmosphere', themenfeld_id: null, text_en: 'early morning blue hour, soft cool light, calm and quiet atmosphere, slight fog' }, { id: '97bad727-6555-4f48-9a68-17dd5ce85535', type: 'atmosphere', text_en: 'early morning blue hour, soft cool light, calm and quiet atmosphere, slight fog' },
{ id: '6de167ef-5a87-4333-9325-cc31ccd9db05', type: 'atmosphere', themenfeld_id: null, text_en: 'warm summer evening, golden orange glow, long shadows, relaxed atmosphere' }, { id: '6de167ef-5a87-4333-9325-cc31ccd9db05', type: 'atmosphere', text_en: 'warm summer evening, golden orange glow, long shadows, relaxed atmosphere' },
{ id: '082cc098-4c26-4d9a-b3a1-209dd9e507ea', type: 'setting', themenfeld_id: '32114354-08f2-4218-920b-ddac63ad6de7', text_en: 'open green meadow with wooden fence, rolling hills in soft background, natural habitat' }, { id: '082cc098-4c26-4d9a-b3a1-209dd9e507ea', type: 'setting', text_en: 'open green meadow with wooden fence, rolling hills in soft background, natural habitat' },
{ id: 'f0ef007a-c763-4c40-99c0-1bd17901739e', type: 'setting', themenfeld_id: '32114354-08f2-4218-920b-ddac63ad6de7', text_en: 'dense forest edge with dappled light, mossy ground, wild and untouched environment' }, { id: 'f0ef007a-c763-4c40-99c0-1bd17901739e', type: 'setting', text_en: 'dense forest edge with dappled light, mossy ground, wild and untouched environment' },
{ id: 'b809f859-2592-4207-8111-7da05e7057c9', type: 'setting', themenfeld_id: '32114354-08f2-4218-920b-ddac63ad6de7', text_en: 'cozy living room corner, warm home environment, soft natural light from window' }, { id: 'b809f859-2592-4207-8111-7da05e7057c9', type: 'setting', text_en: 'cozy living room corner, warm home environment, soft natural light from window' },
{ id: '28dac228-c335-46d2-9b40-481dc9e2b373', type: 'setting', themenfeld_id: '32114354-08f2-4218-920b-ddac63ad6de7', text_en: 'shallow clear river bank, rocky ground, water reflections, natural wetland' }, { id: '28dac228-c335-46d2-9b40-481dc9e2b373', type: 'setting', text_en: 'shallow clear river bank, rocky ground, water reflections, natural wetland' },
{ id: '89cfbdf7-7fbc-439a-9265-73f18124e372', type: 'setting', themenfeld_id: '733bdeee-37a7-4e26-9a74-5e461e7c604d', text_en: 'rustic wooden kitchen counter, natural light from nearby window, linen cloth underneath' }, { id: '89cfbdf7-7fbc-439a-9265-73f18124e372', type: 'setting', text_en: 'rustic wooden kitchen counter, natural light from nearby window, linen cloth underneath' },
{ id: 'e7faf2ec-78e1-43bc-b870-c363f7ec2032', type: 'setting', themenfeld_id: '733bdeee-37a7-4e26-9a74-5e461e7c604d', text_en: 'outdoor farmers market stall, weathered wooden crates, morning light, earthy atmosphere' }, { id: 'e7faf2ec-78e1-43bc-b870-c363f7ec2032', type: 'setting', text_en: 'outdoor farmers market stall, weathered wooden crates, morning light, earthy atmosphere' },
{ id: '45dc2aee-d223-4952-943d-cdbe86b7e8c3', type: 'setting', themenfeld_id: '733bdeee-37a7-4e26-9a74-5e461e7c604d', text_en: 'garden harvest scene, soil and greenery visible, freshly picked produce on ground' }, { id: '45dc2aee-d223-4952-943d-cdbe86b7e8c3', type: 'setting', text_en: 'garden harvest scene, soil and greenery visible, freshly picked produce on ground' },
{ id: '5589aa12-ee74-4041-9443-40e9cfa538fd', type: 'setting', themenfeld_id: '733bdeee-37a7-4e26-9a74-5e461e7c604d', text_en: 'simple white kitchen table, clean minimal background, soft indoor daylight' }, { id: '5589aa12-ee74-4041-9443-40e9cfa538fd', type: 'setting', text_en: 'simple white kitchen table, clean minimal background, soft indoor daylight' },
{ id: '738365f1-b000-4dde-8e99-9b90f6984b79', type: 'setting', themenfeld_id: '294a30e8-a284-4a4e-a257-c4f11d71ce80', text_en: 'neutral light studio setting, clean background, soft natural sidelight, medical clarity' }, { id: '738365f1-b000-4dde-8e99-9b90f6984b79', type: 'setting', text_en: 'neutral light studio setting, clean background, soft natural sidelight, medical clarity' },
{ id: '98f1c118-b333-43ba-9167-870af883b5ae', type: 'setting', themenfeld_id: '294a30e8-a284-4a4e-a257-c4f11d71ce80', text_en: 'warm bathroom environment, mirror and soft light, everyday personal care setting' }, { id: '98f1c118-b333-43ba-9167-870af883b5ae', type: 'setting', text_en: 'warm bathroom environment, mirror and soft light, everyday personal care setting' },
{ id: '2b81a5c9-7328-41e9-b08e-0d98d9a5c78f', type: 'setting', themenfeld_id: '362e5bfe-4394-4b37-8e3c-5b186bd80384', text_en: 'flat lay on light wooden surface, natural window light, clean and minimal styling' }, { id: '2b81a5c9-7328-41e9-b08e-0d98d9a5c78f', type: 'setting', text_en: 'flat lay on light wooden surface, natural window light, clean and minimal styling' },
{ id: '2a3a4eed-ba32-4b21-8dad-1cf5679b00fb', type: 'setting', themenfeld_id: '362e5bfe-4394-4b37-8e3c-5b186bd80384', text_en: 'cozy bedroom setting, clothes laid out on bed, soft morning light' }, { id: '2a3a4eed-ba32-4b21-8dad-1cf5679b00fb', type: 'setting', text_en: 'cozy bedroom setting, clothes laid out on bed, soft morning light' },
{ id: 'c816e95e-5edc-4ae9-8c0d-9c71a5a4dfb6', type: 'setting', themenfeld_id: '362e5bfe-4394-4b37-8e3c-5b186bd80384', text_en: 'outdoor market rack, hangers visible, casual everyday atmosphere' }, { id: 'c816e95e-5edc-4ae9-8c0d-9c71a5a4dfb6', type: 'setting', text_en: 'outdoor market rack, hangers visible, casual everyday atmosphere' },
{ id: '33af0241-c19d-4429-91b5-0359c1f973e4', type: 'setting', themenfeld_id: '6f64102f-8405-4438-861a-fdb438a902b7', text_en: 'warm living room, family home atmosphere, soft afternoon light through curtains' }, { id: '33af0241-c19d-4429-91b5-0359c1f973e4', type: 'setting', text_en: 'warm living room, family home atmosphere, soft afternoon light through curtains' },
{ id: '153e70c4-f011-42af-ba0f-8ab82bf920ab', type: 'setting', themenfeld_id: '6f64102f-8405-4438-861a-fdb438a902b7', text_en: 'outdoor garden or backyard, relaxed family setting, natural daylight' }, { id: '153e70c4-f011-42af-ba0f-8ab82bf920ab', type: 'setting', text_en: 'outdoor garden or backyard, relaxed family setting, natural daylight' },
{ id: '9fe7fc4a-6578-4ee0-8a8e-a885e89e58c1', type: 'setting', themenfeld_id: '7c08ea8d-06c7-4b24-99f7-d282044a62ad', text_en: 'bright kitchen countertop, clean and organized, natural window light' }, { id: '9fe7fc4a-6578-4ee0-8a8e-a885e89e58c1', type: 'setting', text_en: 'bright kitchen countertop, clean and organized, natural window light' },
{ id: '46dab63b-7b3d-45e7-9ea9-4a4a67e9fabd', type: 'setting', themenfeld_id: '7c08ea8d-06c7-4b24-99f7-d282044a62ad', text_en: 'utility room or bathroom shelf, everyday cleaning supplies visible, practical setting' }, { id: '46dab63b-7b3d-45e7-9ea9-4a4a67e9fabd', type: 'setting', text_en: 'utility room or bathroom shelf, everyday cleaning supplies visible, practical setting' },
{ id: '28246e90-4ac8-444f-be23-de401365d38d', type: 'setting', themenfeld_id: '3a781e6a-1900-453a-99b0-ddb15962200d', text_en: 'cozy Scandinavian living room, warm tones, natural materials, soft indirect light' }, { id: '28246e90-4ac8-444f-be23-de401365d38d', type: 'setting', text_en: 'cozy Scandinavian living room, warm tones, natural materials, soft indirect light' },
{ id: '5143c10f-d717-4698-88f5-f1598d0eeef9', type: 'setting', themenfeld_id: '3a781e6a-1900-453a-99b0-ddb15962200d', text_en: 'bright airy bedroom, white walls, minimal furniture, morning sunlight' }, { id: '5143c10f-d717-4698-88f5-f1598d0eeef9', type: 'setting', text_en: 'bright airy bedroom, white walls, minimal furniture, morning sunlight' },
{ id: 'd23d7050-dc22-4226-8a5b-79e75f11de8b', type: 'setting', themenfeld_id: '88b0eb99-88b7-4edd-9d67-496381e5adc7', text_en: 'open countryside landscape, wide sky, natural untouched terrain, peaceful atmosphere' }, { id: 'd23d7050-dc22-4226-8a5b-79e75f11de8b', type: 'setting', text_en: 'open countryside landscape, wide sky, natural untouched terrain, peaceful atmosphere' },
{ id: '34c6a784-7a32-4f84-a06d-f546c9c9fbea', type: 'setting', themenfeld_id: '88b0eb99-88b7-4edd-9d67-496381e5adc7', text_en: 'forest floor close-up, mossy rocks, fallen leaves, soft filtered light through canopy' }, { id: '34c6a784-7a32-4f84-a06d-f546c9c9fbea', type: 'setting', text_en: 'forest floor close-up, mossy rocks, fallen leaves, soft filtered light through canopy' },
{ id: '1fc61dd9-57c6-4eba-8328-37cbf5fc135e', type: 'setting', themenfeld_id: '88b0eb99-88b7-4edd-9d67-496381e5adc7', text_en: 'garden bed with rich dark soil, plants at various growth stages, earthy tones' }, { id: '1fc61dd9-57c6-4eba-8328-37cbf5fc135e', type: 'setting', text_en: 'garden bed with rich dark soil, plants at various growth stages, earthy tones' },
{ id: '3244f090-f2a2-4806-875a-88038598fc5e', type: 'setting', themenfeld_id: '606606e2-5362-4e43-9657-3ad315e323af', text_en: 'quiet suburban street, cobblestone or asphalt road, parked vehicles, everyday scene' }, { id: '3244f090-f2a2-4806-875a-88038598fc5e', type: 'setting', text_en: 'quiet suburban street, cobblestone or asphalt road, parked vehicles, everyday scene' },
{ id: '36d80c19-13ea-4672-b2e9-8ceedb4ab178', type: 'setting', themenfeld_id: '606606e2-5362-4e43-9657-3ad315e323af', text_en: 'rural road with open fields, minimal traffic, wide sky, natural light' }, { id: '36d80c19-13ea-4672-b2e9-8ceedb4ab178', type: 'setting', text_en: 'rural road with open fields, minimal traffic, wide sky, natural light' },
{ id: '98957b0a-f415-4282-9b3d-863a9bf03a77', type: 'setting', themenfeld_id: '5d9e7af6-7866-422e-ac62-b918cfcb2e1a', text_en: 'busy European city street, historic buildings in background, natural daylight' }, { id: '98957b0a-f415-4282-9b3d-863a9bf03a77', type: 'setting', text_en: 'busy European city street, historic buildings in background, natural daylight' },
{ id: '66fa361a-e062-4adc-9c9a-3e01ac8dbbe0', type: 'setting', themenfeld_id: '5d9e7af6-7866-422e-ac62-b918cfcb2e1a', text_en: 'quiet town square, fountain or bench visible, calm everyday atmosphere' }, { id: '66fa361a-e062-4adc-9c9a-3e01ac8dbbe0', type: 'setting', text_en: 'quiet town square, fountain or bench visible, calm everyday atmosphere' },
{ id: '2dba4303-c743-419f-a7e8-06b6d54ba91d', type: 'setting', themenfeld_id: '852f44a4-7930-459e-9a8d-4a44cd895285', text_en: 'clean modern workspace, desk surface, natural sidelight, organized tools' }, { id: '2dba4303-c743-419f-a7e8-06b6d54ba91d', type: 'setting', text_en: 'clean modern workspace, desk surface, natural sidelight, organized tools' },
{ id: 'a78df43b-8897-40dd-9ccf-de29ff9bf5da', type: 'setting', themenfeld_id: '852f44a4-7930-459e-9a8d-4a44cd895285', text_en: 'garage or workshop setting, workbench with tools, practical everyday environment' }, { id: 'a78df43b-8897-40dd-9ccf-de29ff9bf5da', type: 'setting', text_en: 'garage or workshop setting, workbench with tools, practical everyday environment' },
{ id: '949774d1-0678-4683-9b8e-e5568f648ba8', type: 'setting', themenfeld_id: '14cad3c8-8194-4a27-8d48-5107c5bd34e4', text_en: 'outdoor park or sports field, open space, natural daylight, active atmosphere' }, { id: '949774d1-0678-4683-9b8e-e5568f648ba8', type: 'setting', text_en: 'outdoor park or sports field, open space, natural daylight, active atmosphere' },
{ id: '9b35a717-03dd-41aa-a60e-90dff8bc5aaf', type: 'setting', themenfeld_id: '14cad3c8-8194-4a27-8d48-5107c5bd34e4', text_en: 'cozy indoor hobby room, soft warm light, creative materials visible' }, { id: '9b35a717-03dd-41aa-a60e-90dff8bc5aaf', type: 'setting', text_en: 'cozy indoor hobby room, soft warm light, creative materials visible' },
]; ];
for (const s of seeds) { for (const s of seeds) {
await query( await query(
`INSERT INTO prompt_styles (id, type, themenfeld_id, text_en) `INSERT INTO prompt_styles (id, type, text_en)
SELECT $1, $2, $3, $4 SELECT $1, $2, $3
WHERE NOT EXISTS (SELECT 1 FROM prompt_styles WHERE id = $1)`, WHERE NOT EXISTS (SELECT 1 FROM prompt_styles WHERE id = $1)`,
[s.id, s.type, s.themenfeld_id, s.text_en] [s.id, s.type, s.text_en]
).catch(() => {});
}
// kategorie_id per Kategoriename befüllen (idempotent, unabhängig von Category-UUIDs)
const THEME_MAP = [
{ en: 'Animals', ids: ['082cc098-4c26-4d9a-b3a1-209dd9e507ea', 'f0ef007a-c763-4c40-99c0-1bd17901739e', 'b809f859-2592-4207-8111-7da05e7057c9', '28dac228-c335-46d2-9b40-481dc9e2b373'] },
{ en: 'Food', ids: ['89cfbdf7-7fbc-439a-9265-73f18124e372', 'e7faf2ec-78e1-43bc-b870-c363f7ec2032', '45dc2aee-d223-4952-943d-cdbe86b7e8c3', '5589aa12-ee74-4041-9443-40e9cfa538fd'] },
{ en: 'Body', ids: ['738365f1-b000-4dde-8e99-9b90f6984b79', '98f1c118-b333-43ba-9167-870af883b5ae'] },
{ en: 'Clothing', ids: ['2b81a5c9-7328-41e9-b08e-0d98d9a5c78f', '2a3a4eed-ba32-4b21-8dad-1cf5679b00fb', 'c816e95e-5edc-4ae9-8c0d-9c71a5a4dfb6'] },
{ en: 'Family & People', ids: ['33af0241-c19d-4429-91b5-0359c1f973e4', '153e70c4-f011-42af-ba0f-8ab82bf920ab'] },
{ en: 'Household', ids: ['9fe7fc4a-6578-4ee0-8a8e-a885e89e58c1', '46dab63b-7b3d-45e7-9ea9-4a4a67e9fabd'] },
{ en: 'Home & Furniture', ids: ['28246e90-4ac8-444f-be23-de401365d38d', '5143c10f-d717-4698-88f5-f1598d0eeef9'] },
{ en: 'Nature & Plants', ids: ['d23d7050-dc22-4226-8a5b-79e75f11de8b', '34c6a784-7a32-4f84-a06d-f546c9c9fbea', '1fc61dd9-57c6-4eba-8328-37cbf5fc135e'] },
{ en: 'Transport & Travel',ids: ['3244f090-f2a2-4806-875a-88038598fc5e', '36d80c19-13ea-4672-b2e9-8ceedb4ab178'] },
{ en: 'City & Buildings', ids: ['98957b0a-f415-4282-9b3d-863a9bf03a77', '66fa361a-e062-4adc-9c9a-3e01ac8dbbe0'] },
{ en: 'Tools', ids: ['2dba4303-c743-419f-a7e8-06b6d54ba91d', 'a78df43b-8897-40dd-9ccf-de29ff9bf5da'] },
{ en: 'Sports & Leisure', ids: ['949774d1-0678-4683-9b8e-e5568f648ba8', '9b35a717-03dd-41aa-a60e-90dff8bc5aaf'] },
];
for (const { en, ids } of THEME_MAP) {
await query(
`UPDATE prompt_styles
SET kategorie_id = (SELECT id FROM categories WHERE lower(titel_en) = lower($1) LIMIT 1)
WHERE id = ANY($2::uuid[])
AND kategorie_id IS DISTINCT FROM
(SELECT id FROM categories WHERE lower(titel_en) = lower($1) LIMIT 1)`,
[en, ids]
).catch(() => {}); ).catch(() => {});
} }
} }

View File

@@ -31,14 +31,14 @@ router.get('/:id', async (req, res, next) => {
// POST /api/prompt-styles // POST /api/prompt-styles
router.post('/', async (req, res, next) => { router.post('/', async (req, res, next) => {
try { try {
const { type, themenfeld_id, text_en } = req.body; const { type, kategorie_id, text_en } = req.body;
if (!type || !TYPES.includes(type)) if (!type || !TYPES.includes(type))
return res.status(400).json({ error: `type must be one of: ${TYPES.join(', ')}` }); return res.status(400).json({ error: `type must be one of: ${TYPES.join(', ')}` });
if (!text_en) if (!text_en)
return res.status(400).json({ error: 'text_en is required' }); return res.status(400).json({ error: 'text_en is required' });
const result = await query( const result = await query(
`INSERT INTO prompt_styles (type, themenfeld_id, text_en) VALUES ($1, $2, $3) RETURNING *`, `INSERT INTO prompt_styles (type, kategorie_id, text_en) VALUES ($1, $2, $3) RETURNING *`,
[type, themenfeld_id || null, text_en] [type, kategorie_id || null, text_en]
); );
res.status(201).json(result.rows[0]); res.status(201).json(result.rows[0]);
} catch (err) { next(err); } } catch (err) { next(err); }
@@ -47,7 +47,7 @@ router.post('/', async (req, res, next) => {
// PATCH /api/prompt-styles/:id // PATCH /api/prompt-styles/:id
router.patch('/:id', async (req, res, next) => { router.patch('/:id', async (req, res, next) => {
try { try {
const allowed = ['type', 'themenfeld_id', 'text_en']; const allowed = ['type', 'kategorie_id', 'text_en'];
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' });
if (req.body.type && !TYPES.includes(req.body.type)) if (req.body.type && !TYPES.includes(req.body.type))