migrate: backfill old {{uuid}} placeholders to new {{label.w/o:uuid}} format

Runs at startup (idempotent) — only touches rows that still contain bare
{{uuid}} placeholders. Looks up each UUID in words first, then objects,
and rewrites to {{label.w:uuid}} or {{label.o:uuid}} accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 15:00:04 +02:00
parent b57d69fa8e
commit 3147191f55

View File

@@ -494,7 +494,93 @@ async function migrate() {
AND bbox_x IS NULL AND bbox_x IS NULL
`).catch(() => {}); `).catch(() => {});
// ── Migrate old {{uuid}} placeholders → new {{label.w:uuid}} / {{label.o:uuid}} ──
await migratePlaceholders();
console.log('Migration complete'); console.log('Migration complete');
} }
// UUID regex — matches bare {{uuid}} but NOT already-migrated {{label.w:uuid}}
const UUID_RE = /\{\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}\}/gi;
async function migratePlaceholders() {
const textCols = {
questions: ['sentence_de', 'sentence_en', 'sentence_sv'],
statements: [
'positive_sentence_de', 'positive_sentence_en', 'positive_sentence_sv',
'negative_sentence_de', 'negative_sentence_en', 'negative_sentence_sv',
],
};
const uuidSet = new Set();
const affected = {};
for (const [table, cols] of Object.entries(textCols)) {
const whereClause = cols
.map(c => `${c} ~ '\\{\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}\\}'`)
.join(' OR ');
const { rows } = await query(`SELECT id, ${cols.join(', ')} FROM ${table} WHERE ${whereClause}`);
if (rows.length) {
affected[table] = rows;
rows.forEach(row => cols.forEach(col => {
for (const m of (row[col] || '').matchAll(UUID_RE)) uuidSet.add(m[1]);
}));
}
}
if (uuidSet.size === 0) return;
const uuids = [...uuidSet];
const labelMap = {};
// Words first
const { rows: wordRows } = await query(
`SELECT id, titel_de, titel_en FROM words WHERE id = ANY($1::uuid[])`, [uuids]
);
wordRows.forEach(w => { labelMap[w.id] = { label: w.titel_de || w.titel_en || 'Wort', type: 'w' }; });
// Remaining → objects
const missing = uuids.filter(id => !labelMap[id]);
if (missing.length) {
const { rows: objRows } = await query(
`SELECT o.id, w.titel_de, w.titel_en
FROM objects o
LEFT JOIN object_words ow ON ow.object_id = o.id
LEFT JOIN words w ON w.id = ow.word_id
WHERE o.id = ANY($1::uuid[])`, [missing]
);
const seen = new Set();
objRows.forEach(r => {
if (!seen.has(r.id)) {
seen.add(r.id);
labelMap[r.id] = { label: r.titel_de || r.titel_en || 'Objekt', type: 'o' };
}
});
}
// UPDATE affected rows
for (const [table, rows] of Object.entries(affected)) {
const cols = textCols[table];
for (const row of rows) {
const updates = {};
for (const col of cols) {
const text = row[col];
if (!text) continue;
const replaced = text.replace(UUID_RE, (_, uuid) => {
const info = labelMap[uuid];
return info ? `{{${info.label}.${info.type}:${uuid}}}` : `{{${uuid}}}`;
});
if (replaced !== text) updates[col] = replaced;
}
if (Object.keys(updates).length) {
const setClauses = Object.keys(updates).map((k, i) => `${k} = $${i + 2}`).join(', ');
await query(`UPDATE ${table} SET ${setClauses} WHERE id = $1`, [row.id, ...Object.values(updates)]);
}
}
}
const count = Object.values(affected).reduce((s, r) => s + r.length, 0);
if (count > 0) console.log(`Placeholder migration: updated ${count} rows`);
}
module.exports = migrate; module.exports = migrate;