/** * Schema-Migration für PocketBase * * Erweitert das vorhandene Schema und migriert bestehende Daten: * - events: + name (Text), + status (Select draft/active/finished) * Daten: description → name kopieren, status = 'draft' * - riders: + number (Text), + firstname (Text), + lastname (Text) * Daten: bestehender name-Wert → number kopieren * - times: + correction (Number, Sekunden) * status erweitern: + dnf, dns, dsq * * Aufruf: npx tsx scripts/migrate-schema.ts */ import { readFileSync } from 'node:fs' // .env minimal selbst parsen for (const line of readFileSync('.env', 'utf8').split('\n')) { const m = line.match(/^([A-Z_]+)=(.*)$/) if (m && !process.env[m[1]]) process.env[m[1]] = m[2] } const API = process.env.PB_TYPEGEN_URL?.replace(/\/$/, '') ?? 'https://api.stammtisch-hersbruck.de' const TOKEN = process.env.PB_SUPERUSER_TOKEN if (!TOKEN) { console.error('PB_SUPERUSER_TOKEN fehlt in .env') process.exit(1) } const headers = { 'Authorization': TOKEN, 'Content-Type': 'application/json', } async function req(method: string, path: string, body?: unknown) { const res = await fetch(`${API}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }) if (!res.ok) { const text = await res.text() throw new Error(`${method} ${path} → ${res.status}: ${text}`) } return res.status === 204 ? null : await res.json() } async function getCollection(name: string) { return req('GET', `/api/collections/${name}`) } async function updateCollection(name: string, body: any) { return req('PATCH', `/api/collections/${name}`, body) } async function listRecords(collection: string) { const all: any[] = [] let page = 1 while (true) { const res: any = await req('GET', `/api/collections/${collection}/records?page=${page}&perPage=200`) all.push(...res.items) if (page >= res.totalPages || res.totalPages === 0) break page++ } return all } async function updateRecord(collection: string, id: string, data: any) { return req('PATCH', `/api/collections/${collection}/records/${id}`, data) } function hasField(coll: any, name: string) { return coll.fields.some((f: any) => f.name === name) } // ---- Migration ---- async function migrateEvents() { console.log('\n== events ==') const coll = await getCollection('events') const fields = [...coll.fields] if (!hasField(coll, 'name')) { fields.push({ name: 'name', type: 'text', required: false, presentable: true, max: 0, min: 0, pattern: '', autogeneratePattern: '', }) console.log(' + Feld "name" (text)') } else { console.log(' = Feld "name" existiert bereits') } if (!hasField(coll, 'status')) { fields.push({ name: 'status', type: 'select', required: false, presentable: false, maxSelect: 1, values: ['draft', 'active', 'finished'], }) console.log(' + Feld "status" (select draft/active/finished)') } else { console.log(' = Feld "status" existiert bereits') } await updateCollection('events', { fields }) // Daten migrieren const records = await listRecords('events') for (const r of records) { const patch: any = {} if (!r.name && r.description) patch.name = r.description if (!r.status) patch.status = 'draft' if (Object.keys(patch).length) { await updateRecord('events', r.id, patch) console.log(` → ${r.id}: ${JSON.stringify(patch)}`) } } } async function migrateRiders() { console.log('\n== riders ==') const coll = await getCollection('riders') const fields = [...coll.fields] if (!hasField(coll, 'number')) { fields.push({ name: 'number', type: 'text', required: false, presentable: true, max: 0, min: 0, pattern: '', autogeneratePattern: '', }) console.log(' + Feld "number" (text)') } else { console.log(' = Feld "number" existiert bereits') } if (!hasField(coll, 'firstname')) { fields.push({ name: 'firstname', type: 'text', required: false, presentable: true, max: 0, min: 0, pattern: '', autogeneratePattern: '', }) console.log(' + Feld "firstname" (text)') } else { console.log(' = Feld "firstname" existiert bereits') } if (!hasField(coll, 'lastname')) { fields.push({ name: 'lastname', type: 'text', required: false, presentable: true, max: 0, min: 0, pattern: '', autogeneratePattern: '', }) console.log(' + Feld "lastname" (text)') } else { console.log(' = Feld "lastname" existiert bereits') } await updateCollection('riders', { fields }) // Daten migrieren: name → number, falls number leer const records = await listRecords('riders') for (const r of records) { if (!r.number && r.name) { await updateRecord('riders', r.id, { number: r.name }) console.log(` → ${r.id}: number = "${r.name}"`) } } } async function migrateTimes() { console.log('\n== times ==') const coll = await getCollection('times') const fields = [...coll.fields] let changed = false if (!hasField(coll, 'correction')) { fields.push({ name: 'correction', type: 'number', required: false, presentable: false, min: null, max: null, onlyInt: false, }) console.log(' + Feld "correction" (number)') changed = true } else { console.log(' = Feld "correction" existiert bereits') } // status um dnf, dns, dsq erweitern const statusField = fields.find((f: any) => f.name === 'status') if (statusField) { const current: string[] = statusField.values || [] const target = ['active', 'finished', 'dnf', 'dns', 'dsq'] const missing = target.filter((v) => !current.includes(v)) if (missing.length) { statusField.values = [...current, ...missing] console.log(` ~ Feld "status" erweitert um: ${missing.join(', ')}`) changed = true } else { console.log(' = Feld "status" hat bereits alle Werte') } } if (changed) await updateCollection('times', { fields }) } async function main() { console.log(`Migration gegen ${API}`) await migrateEvents() await migrateRiders() await migrateTimes() console.log('\n✓ Migration fertig') } main().catch((e) => { console.error('\n✗ Fehler:', e.message) process.exit(1) })