chore: veraltete Scripts unter frontend/scripts entfernen
Alle vier Scripts setzten auf einem Schema auf, das es nicht mehr gibt: - seed-test-data.ts schrieb in stages und results (heute: runs und times) und kannte users.role, riders.email/birthday sowie events.created_by/ timekeepers -- alles nicht mehr vorhanden. Teams kannte es gar nicht, ohne Team-Zuordnung greifen sämtliche API-Rules und die Daten wären unsichtbar geblieben. - migrate-schema.ts und migrate-teams.ts waren Einmal-Migrationen. Ihr Ergebnis steckt längst im Schema (events.status, riders.number, times. correction, die teams-Collection, die team-Felder). Ein erneuter Lauf gegen die produktive Instanz wäre bestenfalls wirkungslos. - test-token.ts war ein Wegwerf-Prüfscript für den Superuser-Token. Das Schema ist jetzt in backend/pb_migrations/ versioniert; das ist der Weg für Änderungen. tsx entfällt als Abhängigkeit, es wurde nur von diesen Scripts benutzt. PB_SUPERUSER_TOKEN bleibt in .env -- generate-pocketbase-types braucht ihn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1061a8b9ad
commit
5658ef7bcc
7 changed files with 7 additions and 1019 deletions
|
|
@ -38,19 +38,12 @@ Die Variable wird von SvelteKit zur **Buildzeit** eingesetzt
|
|||
Neustart des Dev-Servers bzw. einen neuen Build — ein blosser Neustart des
|
||||
Containers genügt nicht.
|
||||
|
||||
`PB_TYPEGEN_URL` und `PB_SUPERUSER_TOKEN` gelten getrennt davon für die
|
||||
Typgenerierung und die Scripts unter `scripts/`.
|
||||
`PB_TYPEGEN_URL` und `PB_SUPERUSER_TOKEN` gelten getrennt davon für
|
||||
`npm run generate-pocketbase-types`.
|
||||
|
||||
## Scripts
|
||||
|
||||
Die Dateien unter `scripts/` sind Einmal-Werkzeuge gegen eine PocketBase-Instanz
|
||||
und laufen über `tsx`. Sie lesen ihre Zugangsdaten aus `.env`.
|
||||
|
||||
> **Achtung, veralteter Stand:** `seed-test-data.ts` (`npm run seed`),
|
||||
> `migrate-schema.ts` und `migrate-teams.ts` stammen aus einer früheren
|
||||
> Schema-Generation. `seed-test-data.ts` schreibt in die Collections `stages`
|
||||
> und `results`, die es im aktuellen Schema **nicht mehr gibt** — der Aufruf
|
||||
> schlägt fehl. Vor einer Verwendung müssen die Scripts auf das aktuelle Schema
|
||||
> (`users`, `teams`, `events`, `runs`, `riders`, `times`) angepasst werden.
|
||||
## Schema
|
||||
|
||||
Das maßgebliche Schema liegt als Migration in `../backend/pb_migrations/`.
|
||||
Nach einer Schema-Änderung `npm run generate-pocketbase-types` ausführen — das
|
||||
aktualisiert `src/lib/types.d.ts`, aus dem der PocketBase-Client seine Typen
|
||||
bezieht.
|
||||
|
|
|
|||
44
frontend/package-lock.json
generated
44
frontend/package-lock.json
generated
|
|
@ -31,7 +31,6 @@
|
|||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tsx": "^4.19.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.7"
|
||||
|
|
@ -2379,19 +2378,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
|
|
@ -3914,16 +3900,6 @@
|
|||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
|
|
@ -4646,26 +4622,6 @@
|
|||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.6",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
|
||||
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@
|
|||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"generate-pocketbase-types": "set -a && . ./.env && set +a && pocketbase-typegen --url \"$PB_TYPEGEN_URL\" --token \"$PB_SUPERUSER_TOKEN\" --out ./src/lib/types.d.ts",
|
||||
"seed": "tsx scripts/seed-test-data.ts",
|
||||
"migrate": "tsx scripts/migrate-schema.ts",
|
||||
"migrate-teams": "tsx scripts/migrate-teams.ts"
|
||||
"generate-pocketbase-types": "set -a && . ./.env && set +a && pocketbase-typegen --url \"$PB_TYPEGEN_URL\" --token \"$PB_SUPERUSER_TOKEN\" --out ./src/lib/types.d.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.12.1",
|
||||
|
|
@ -32,7 +29,6 @@
|
|||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tsx": "^4.19.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.7"
|
||||
|
|
|
|||
|
|
@ -1,245 +0,0 @@
|
|||
/**
|
||||
* 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)
|
||||
})
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
/**
|
||||
* 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<string>([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)
|
||||
})
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
/**
|
||||
* Test Data Seeding Script for PocketBase
|
||||
*
|
||||
* This script creates initial test data for the timing app:
|
||||
* - Admin user
|
||||
* - Timekeeper users
|
||||
* - Test riders
|
||||
* - Sample event with stages
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/seed-test-data.ts
|
||||
*
|
||||
* Environment Variables (from .env):
|
||||
* PB_TYPEGEN_URL - PocketBase API URL
|
||||
* PB_TYPEGEN_TOKEN - Authentication token for impersonation
|
||||
*/
|
||||
|
||||
import PocketBase from 'pocketbase'
|
||||
import type { TypedPocketBase } from '../src/lib/types.js'
|
||||
|
||||
// Validate required environment variables
|
||||
const PB_URL = process.env.PB_TYPEGEN_URL
|
||||
const PB_TOKEN = process.env.PB_TYPEGEN_TOKEN
|
||||
|
||||
if (!PB_URL) {
|
||||
console.error('❌ Error: PB_TYPEGEN_URL is not set in .env file')
|
||||
console.error(' Please set PB_TYPEGEN_URL in your .env file')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!PB_TOKEN) {
|
||||
console.error('❌ Error: PB_TYPEGEN_TOKEN is not set in .env file')
|
||||
console.error(' Please set PB_TYPEGEN_TOKEN in your .env file')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Initialize PocketBase with URL from .env
|
||||
const pb = new PocketBase(PB_URL) as TypedPocketBase
|
||||
|
||||
// Authenticate using token from .env (impersonation)
|
||||
pb.authStore.save(PB_TOKEN)
|
||||
|
||||
console.log(`🔗 Connecting to PocketBase at: ${PB_URL}`)
|
||||
console.log(`🔑 Using authentication token from .env\n`)
|
||||
|
||||
// Configuration
|
||||
const ADMIN_EMAIL = 'admin@stammtisch-hersbruck.de'
|
||||
const ADMIN_PASSWORD = 'Admin123!secure'
|
||||
|
||||
async function seedData() {
|
||||
console.log('🌱 Starting test data seeding...\n')
|
||||
|
||||
try {
|
||||
// 1. Create Admin User
|
||||
console.log('👤 Creating admin user...')
|
||||
let adminUser
|
||||
try {
|
||||
adminUser = await pb.collection('users').create({
|
||||
email: ADMIN_EMAIL,
|
||||
password: ADMIN_PASSWORD,
|
||||
passwordConfirm: ADMIN_PASSWORD,
|
||||
name: 'Administrator',
|
||||
role: 'admin',
|
||||
verified: true
|
||||
})
|
||||
console.log(`✅ Admin created: ${adminUser.email}`)
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('already exists')) {
|
||||
console.log('⚠️ Admin user already exists, logging in...')
|
||||
await pb.collection('users').authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD)
|
||||
adminUser = pb.authStore.record
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Create Timekeeper Users
|
||||
console.log('\n⏱️ Creating timekeeper users...')
|
||||
const timekeepers = []
|
||||
const timekeeperData = [
|
||||
{ email: 'timer1@stammtisch-hersbruck.de', name: 'Max Zeitnehmer', password: 'Timer123!' },
|
||||
{ email: 'timer2@stammtisch-hersbruck.de', name: 'Anna Stoppuhr', password: 'Timer123!' },
|
||||
{ email: 'timer3@stammtisch-hersbruck.de', name: 'Tom Chronograph', password: 'Timer123!' }
|
||||
]
|
||||
|
||||
for (const data of timekeeperData) {
|
||||
try {
|
||||
const user = await pb.collection('users').create({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
passwordConfirm: data.password,
|
||||
name: data.name,
|
||||
role: 'timekeeper',
|
||||
verified: true
|
||||
})
|
||||
timekeepers.push(user)
|
||||
console.log(`✅ Timekeeper created: ${user.name} (${user.email})`)
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('already exists')) {
|
||||
console.log(`⚠️ Timekeeper ${data.email} already exists, skipping...`)
|
||||
// Fetch existing user
|
||||
const existing = await pb.collection('users').getFirstListItem(`email="${data.email}"`)
|
||||
timekeepers.push(existing)
|
||||
} else {
|
||||
console.error(`❌ Error creating ${data.email}:`, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create Test Riders
|
||||
console.log('\n🚴 Creating test riders...')
|
||||
const riders = []
|
||||
const riderData = [
|
||||
{ number: '1', name: 'Max Müller', email: 'max.mueller@example.com', birthday: '1995-03-15' },
|
||||
{ number: '2', name: 'Anna Schmidt', email: 'anna.schmidt@example.com', birthday: '1992-07-22' },
|
||||
{ number: '3', name: 'Tom Weber', email: 'tom.weber@example.com', birthday: '1998-11-08' },
|
||||
{ number: '17', name: 'Lisa Hoffmann', email: 'lisa.hoffmann@example.com', birthday: '2000-01-30' },
|
||||
{ number: '23', name: 'Felix Wagner', email: 'felix.wagner@example.com', birthday: '1996-05-12' },
|
||||
{ number: '42', name: 'Sarah Becker', email: 'sarah.becker@example.com', birthday: '1994-09-25' },
|
||||
{ number: '99', name: 'Lukas Fischer', email: 'lukas.fischer@example.com', birthday: '1999-12-03' },
|
||||
{ number: '101', name: 'Julia Schulz', email: 'julia.schulz@example.com', birthday: '1997-04-18' }
|
||||
]
|
||||
|
||||
for (const data of riderData) {
|
||||
try {
|
||||
const rider = await pb.collection('riders').create({
|
||||
number: data.number,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
birthday: data.birthday
|
||||
})
|
||||
riders.push(rider)
|
||||
console.log(`✅ Rider created: #${rider.number} ${rider.name}`)
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('already exists')) {
|
||||
console.log(`⚠️ Rider #${data.number} already exists, skipping...`)
|
||||
const existing = await pb.collection('riders').getFirstListItem(`number="${data.number}"`)
|
||||
riders.push(existing)
|
||||
} else {
|
||||
console.error(`❌ Error creating rider #${data.number}:`, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Create Test Event
|
||||
console.log('\n🏁 Creating test event...')
|
||||
let event
|
||||
try {
|
||||
// Event date: 2 weeks from now
|
||||
const eventDate = new Date()
|
||||
eventDate.setDate(eventDate.getDate() + 14)
|
||||
|
||||
const startTime = new Date(eventDate)
|
||||
startTime.setHours(9, 0, 0, 0)
|
||||
|
||||
const endTime = new Date(eventDate)
|
||||
endTime.setHours(17, 0, 0, 0)
|
||||
|
||||
event = await pb.collection('events').create({
|
||||
name: 'MTB Hersbruck Cup 2025',
|
||||
location: 'Hersbruck, Deutschland',
|
||||
start: startTime.toISOString(),
|
||||
end: endTime.toISOString(),
|
||||
status: 'draft',
|
||||
created_by: adminUser?.id,
|
||||
timekeepers: timekeepers.map(t => t.id)
|
||||
})
|
||||
console.log(`✅ Event created: ${event.name}`)
|
||||
console.log(` Date: ${new Date(event.start).toLocaleDateString('de-DE')}`)
|
||||
console.log(` Timekeepers: ${timekeepers.length} assigned`)
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('already exists')) {
|
||||
console.log('⚠️ Event already exists, fetching...')
|
||||
event = await pb.collection('events').getFirstListItem('name~"MTB Hersbruck"')
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Create Stages
|
||||
console.log('\n🏔️ Creating stages...')
|
||||
const stages = []
|
||||
const stageData = [
|
||||
{
|
||||
name: 'Downhill',
|
||||
location: 'Steinberg Trail',
|
||||
order: 1,
|
||||
start_number: 1,
|
||||
end_number: 50,
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
name: 'Cross-Country',
|
||||
location: 'Wald-Rundkurs',
|
||||
order: 2,
|
||||
start_number: 51,
|
||||
end_number: 100,
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
name: 'Enduro Stage 1',
|
||||
location: 'Berg-Passage',
|
||||
order: 3,
|
||||
start_number: 101,
|
||||
end_number: 150,
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
|
||||
for (const data of stageData) {
|
||||
try {
|
||||
// Stage times based on event date
|
||||
const stageStart = new Date(event.start)
|
||||
stageStart.setHours(stageStart.getHours() + (data.order - 1) * 2)
|
||||
|
||||
const stageEnd = new Date(stageStart)
|
||||
stageEnd.setHours(stageEnd.getHours() + 2)
|
||||
|
||||
const stage = await pb.collection('stages').create({
|
||||
event: event.id,
|
||||
name: data.name,
|
||||
location: data.location,
|
||||
start: stageStart.toISOString(),
|
||||
end: stageEnd.toISOString(),
|
||||
order: data.order,
|
||||
start_number: data.start_number,
|
||||
end_number: data.end_number,
|
||||
status: data.status
|
||||
})
|
||||
stages.push(stage)
|
||||
console.log(`✅ Stage created: ${stage.name} (Startnummern: ${data.start_number}-${data.end_number})`)
|
||||
} catch (e: any) {
|
||||
console.error(`❌ Error creating stage ${data.name}:`, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Create Sample Results (optional - for demo)
|
||||
console.log('\n⏱️ Creating sample results...')
|
||||
|
||||
// Create 2 finished results for demonstration
|
||||
if (stages.length > 0 && riders.length >= 3 && timekeepers.length > 0) {
|
||||
const firstStage = stages[0]
|
||||
const sampleResults = [
|
||||
{
|
||||
rider: riders[0],
|
||||
duration: 185.5, // 3:05.5
|
||||
status: 'finished'
|
||||
},
|
||||
{
|
||||
rider: riders[1],
|
||||
duration: 192.3, // 3:12.3
|
||||
status: 'finished'
|
||||
},
|
||||
{
|
||||
rider: riders[2],
|
||||
duration: 0, // currently running
|
||||
status: 'running'
|
||||
}
|
||||
]
|
||||
|
||||
for (const data of sampleResults) {
|
||||
try {
|
||||
const now = new Date()
|
||||
const startTime = new Date(now.getTime() - (data.duration * 1000) - 60000) // Started some time ago
|
||||
const endTime = data.status === 'finished' ? new Date(startTime.getTime() + data.duration * 1000) : undefined
|
||||
|
||||
const result = await pb.collection('results').create({
|
||||
stage: firstStage.id,
|
||||
rider: data.rider.id,
|
||||
start: startTime.toISOString(),
|
||||
end: endTime?.toISOString() || null,
|
||||
status: data.status,
|
||||
started_by: timekeepers[0].id,
|
||||
stopped_by: data.status === 'finished' ? timekeepers[0].id : null,
|
||||
correction: 0
|
||||
})
|
||||
|
||||
const timeStr = data.status === 'finished'
|
||||
? `${Math.floor(data.duration / 60)}:${(data.duration % 60).toFixed(1).padStart(4, '0')}`
|
||||
: 'RUNNING'
|
||||
console.log(`✅ Result created: #${data.rider.number} ${data.rider.name} - ${timeStr}`)
|
||||
} catch (e: any) {
|
||||
console.error(`❌ Error creating result:`, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(50))
|
||||
console.log('✅ Test data seeding completed!')
|
||||
console.log('='.repeat(50))
|
||||
console.log('\n📊 Summary:')
|
||||
console.log(` Users: ${1 + timekeepers.length} (1 admin, ${timekeepers.length} timekeepers)`)
|
||||
console.log(` Riders: ${riders.length}`)
|
||||
console.log(` Events: 1`)
|
||||
console.log(` Stages: ${stages.length}`)
|
||||
console.log(` Results: 3 (2 finished, 1 running)`)
|
||||
|
||||
console.log('\n🔑 Login Credentials:')
|
||||
console.log(` Admin: ${ADMIN_EMAIL} / ${ADMIN_PASSWORD}`)
|
||||
console.log(` Timekeeper: timer1@stammtisch-hersbruck.de / Timer123!`)
|
||||
console.log(` Timekeeper: timer2@stammtisch-hersbruck.de / Timer123!`)
|
||||
console.log(` Timekeeper: timer3@stammtisch-hersbruck.de / Timer123!`)
|
||||
|
||||
console.log('\n🚀 Next steps:')
|
||||
console.log(' 1. Run: npm run generate-pocketbase-types')
|
||||
console.log(' 2. Start development: npm run dev')
|
||||
console.log(' 3. Login with one of the credentials above')
|
||||
console.log('')
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ Error during seeding:', error)
|
||||
if (error.response) {
|
||||
console.error('Response:', error.response)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the seeding
|
||||
seedData()
|
||||
.then(() => {
|
||||
console.log('✅ Seeding completed successfully!')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Seeding failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Test-Script: prüft was der PB_SUPERUSER_TOKEN tatsächlich darf.
|
||||
* Aufruf: npx tsx scripts/test-token.ts
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
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 TOKEN = process.env.PB_SUPERUSER_TOKEN ?? ''
|
||||
const API = 'https://api.stammtisch-hersbruck.de'
|
||||
|
||||
// JWT-Payload dekodieren
|
||||
const [, payloadB64] = TOKEN.split('.')
|
||||
if (payloadB64) {
|
||||
const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString('utf8'))
|
||||
console.log('Token-Payload:', payload)
|
||||
console.log('Token-Länge:', TOKEN.length)
|
||||
}
|
||||
|
||||
async function tryEndpoint(label: string, path: string, init?: RequestInit) {
|
||||
const res = await fetch(`${API}${path}`, {
|
||||
...init,
|
||||
headers: { Authorization: TOKEN, 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
|
||||
})
|
||||
const text = await res.text()
|
||||
console.log(`\n${label}: ${res.status}`)
|
||||
try {
|
||||
console.log(JSON.stringify(JSON.parse(text), null, 2).slice(0, 200))
|
||||
} catch {
|
||||
console.log(text.slice(0, 200))
|
||||
}
|
||||
}
|
||||
|
||||
await tryEndpoint('GET /api/collections', '/api/collections?perPage=1')
|
||||
await tryEndpoint('GET /api/collections/_superusers', '/api/collections/_superusers')
|
||||
await tryEndpoint('GET /api/collections/events', '/api/collections/events')
|
||||
await tryEndpoint('POST /api/collections (Probe)', '/api/collections', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: '__probe_temp__',
|
||||
type: 'base',
|
||||
fields: [{ name: 'foo', type: 'text' }],
|
||||
}),
|
||||
})
|
||||
Loading…
Reference in a new issue