/** * Schema-Migration: Teams einführen * * - neue Collection 'teams' (name, owner, admins[], users[]) * - events, riders, runs, times bekommen ein 'team'-Feld * - listRule/viewRule auf allen vier wird auf team.users.id ?= @request.auth.id umgestellt * - events.users wird entfernt * - Migration der Daten: * Team "Stammtisch Hersbruck" anlegen * Owner = aktuell eingeloggter User (über .env: PB_OWNER_EMAIL falls gesetzt, sonst erster events.users) * users = Union aller events.users * events/riders/runs/times werden alle diesem Team zugeordnet * * Aufruf: npm run migrate-teams */ import { readFileSync } from 'node:fs' // .env parsen — Zeilen die mit . anfangen werden an die vorherige Variable angehängt // (Workaround für zerbrochene JWT-Tokens über mehrere Zeilen) const env = readFileSync('.env', 'utf8').split('\n') let lastKey = '' for (const line of env) { const m = line.match(/^([A-Z_]+)=(.*)$/) if (m) { process.env[m[1]] = m[2] lastKey = m[1] } else if (lastKey && line.length > 0) { process.env[lastKey] = (process.env[lastKey] ?? '') + line } } 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') 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) throw new Error(`${method} ${path} → ${res.status}: ${await res.text()}`) return res.status === 204 ? null : await res.json() } async function getCollection(name: string) { try { return await req('GET', `/api/collections/${name}`) } catch { return null } } async function createCollection(body: any) { return req('POST', '/api/collections', body) } async function updateCollection(name: string, body: any) { return req('PATCH', `/api/collections/${name}`, body) } async function listAll(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) } async function createRecord(collection: string, data: any) { return req('POST', `/api/collections/${collection}/records`, data) } function hasField(coll: any, name: string) { return coll.fields.some((f: any) => f.name === name) } const USERS_AUTH = '_pb_users_auth_' // ---- Schritt 1: teams Collection anlegen ---- async function ensureTeamsCollection() { console.log('\n== teams Collection ==') const existing = await getCollection('teams') if (existing) { console.log(' = teams existiert bereits') return existing } const body = { name: 'teams', type: 'base', listRule: 'users.id ?= @request.auth.id', viewRule: 'users.id ?= @request.auth.id', createRule: '@request.auth.id != ""', updateRule: 'owner.id ?= @request.auth.id || admins.id ?= @request.auth.id', deleteRule: 'owner.id ?= @request.auth.id', fields: [ { name: 'name', type: 'text', required: true, presentable: true, max: 100, min: 1, pattern: '', autogeneratePattern: '', }, { name: 'owner', type: 'relation', required: true, maxSelect: 1, minSelect: 1, collectionId: USERS_AUTH, cascadeDelete: false, }, { name: 'admins', type: 'relation', required: false, maxSelect: 999, minSelect: 0, collectionId: USERS_AUTH, cascadeDelete: false, }, { name: 'users', type: 'relation', required: false, maxSelect: 999, minSelect: 0, collectionId: USERS_AUTH, cascadeDelete: false, }, { name: 'created', type: 'autodate', onCreate: true, onUpdate: false, }, { name: 'updated', type: 'autodate', onCreate: true, onUpdate: true, }, ], } const created = await createCollection(body) console.log(' + Collection "teams" angelegt') return created } // ---- Schritt 2: team-Feld zu events/riders/runs/times hinzufügen ---- async function addTeamField(collName: string) { const coll = await getCollection(collName) if (!coll) throw new Error(`Collection ${collName} fehlt`) if (hasField(coll, 'team')) { console.log(` = ${collName}.team existiert bereits`) return } const fields = [ ...coll.fields, { name: 'team', type: 'relation', required: false, maxSelect: 1, minSelect: 0, collectionId: (await getCollection('teams'))!.id, cascadeDelete: false, }, ] await updateCollection(collName, { fields }) console.log(` + ${collName}.team hinzugefügt`) } // ---- Schritt 3: Default-Team anlegen + alle Records zuordnen ---- async function migrateData(teamsId: string) { console.log('\n== Daten-Migration ==') // Owner-User suchen const ownerEmail = process.env.PB_OWNER_EMAIL || 'stammtisch-hersbruck@dne.name' const usersRes: any = await req( 'GET', `/api/collections/${USERS_AUTH}/records?filter=` + encodeURIComponent(`email = "${ownerEmail}"`), ) if (!usersRes.items.length) { throw new Error(`User mit E-Mail "${ownerEmail}" nicht gefunden. Setze PB_OWNER_EMAIL in .env.`) } const owner = usersRes.items[0] console.log(` Owner: ${owner.email} (${owner.id})`) // Existierendes Team suchen const teamsList: any = await req('GET', `/api/collections/teams/records?filter=` + encodeURIComponent(`name = "Stammtisch Hersbruck"`)) let team: any = teamsList.items[0] if (!team) { // alle User aus events.users sammeln const events = await listAll('events') const userSet = new Set([owner.id]) for (const e of events) { if (Array.isArray(e.users)) for (const u of e.users) userSet.add(u) } team = await createRecord('teams', { name: 'Stammtisch Hersbruck', owner: owner.id, admins: [], users: [...userSet], }) console.log(` + Team "Stammtisch Hersbruck" mit ${userSet.size} Mitgliedern angelegt`) } else { console.log(` = Team "Stammtisch Hersbruck" existiert (${team.id})`) } // Datensätze ohne team patchen for (const collName of ['events', 'riders', 'runs', 'times']) { const records = await listAll(collName) let touched = 0 for (const r of records) { if (!r.team) { await updateRecord(collName, r.id, { team: team.id }) touched++ } } console.log(` → ${collName}: ${touched} Records zugeordnet (${records.length - touched} bereits gesetzt)`) } return team } // ---- Schritt 4: Rules auf team-basiert umstellen ---- async function updateRules() { console.log('\n== Rules auf team-basiert umstellen ==') const rules = { events: { listRule: 'team.users.id ?= @request.auth.id', viewRule: 'team.users.id ?= @request.auth.id', createRule: 'team.users.id ?= @request.auth.id', updateRule: 'team.users.id ?= @request.auth.id', deleteRule: 'team.users.id ?= @request.auth.id', }, riders: { listRule: 'team.users.id ?= @request.auth.id', viewRule: 'team.users.id ?= @request.auth.id', createRule: 'team.users.id ?= @request.auth.id', updateRule: 'team.users.id ?= @request.auth.id', deleteRule: 'team.users.id ?= @request.auth.id', }, runs: { listRule: 'team.users.id ?= @request.auth.id', viewRule: 'team.users.id ?= @request.auth.id', createRule: 'team.users.id ?= @request.auth.id', updateRule: 'team.users.id ?= @request.auth.id', deleteRule: 'team.users.id ?= @request.auth.id', }, times: { listRule: 'team.users.id ?= @request.auth.id', viewRule: 'team.users.id ?= @request.auth.id', createRule: 'team.users.id ?= @request.auth.id', updateRule: 'team.users.id ?= @request.auth.id', deleteRule: 'team.users.id ?= @request.auth.id', }, } for (const [coll, r] of Object.entries(rules)) { await updateCollection(coll, r) console.log(` ~ ${coll} Rules aktualisiert`) } } // ---- Schritt 5: events.users entfernen ---- async function removeEventsUsers() { console.log('\n== events.users entfernen ==') const coll = await getCollection('events') if (!coll) return if (!hasField(coll, 'users')) { console.log(' = events.users existiert nicht (mehr)') return } const fields = coll.fields.filter((f: any) => f.name !== 'users') await updateCollection('events', { fields }) console.log(' - events.users entfernt') } // ---- Main ---- async function main() { console.log(`Migration gegen ${API}`) const teams = await ensureTeamsCollection() console.log('\n== team-Feld zu Collections hinzufügen ==') await addTeamField('events') await addTeamField('riders') await addTeamField('runs') await addTeamField('times') await migrateData(teams.id) await updateRules() await removeEventsUsers() console.log('\n✓ Teams-Migration fertig') } main().catch((e) => { console.error('\n✗ Fehler:', e.message) process.exit(1) })