Auf einer leeren Datenbank brach der Start des Backends ab, damit war die Anwendung insgesamt nicht benutzbar. Zwei Ursachen, beide nur ohne bestehende pb_data sichtbar, weil angewendete Migrationen nicht erneut laufen. Erstens verwies trail_flags.deleteRule auf @collection.trail_markers, obwohl trail_markers erst danach angelegt wird. Die verschaerfte Regel war nachtraeglich in die bereits angewendete Migration eingebaut worden, zusaetzlich zu den korrekten Folgemigrationen 200/300. Hier steht nun wieder die referenzfreie Ursprungsregel; den Zielzustand setzen die Nachtraege. Zweitens enthaelt 1754400000_init_schema.js als Snapshot des Live-Schemas die trail-Collections bereits, wodurch diese Migration sie ein zweites Mal anlegte und am eindeutigen Collection-Namen scheiterte. Ein Guard ueberspringt sie, wenn trails schon existiert. Verifiziert gegen ein leeres Datenverzeichnis: alle Migrationen laufen durch, alle elf Anwendungs-Collections vorhanden, die Nachtraege korrekt im Endzustand (geklammerte deleteRule, coord_distances, avatar.maxSize), Team-Anlage als normaler User erfolgreich. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
292 lines
10 KiB
JavaScript
292 lines
10 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
// Trails: GPX-Tracks mit Versionen, verorteten Markern und Kommentaren.
|
|
//
|
|
// Reihenfolge ist wichtig: trails.current zeigt auf trail_versions,
|
|
// trail_versions.trail zeigt zurück auf trails. Deshalb wird trails zuerst
|
|
// OHNE current angelegt, dann trail_versions, und current danach ergänzt.
|
|
//
|
|
// 1754400000_init_schema.js wurde nachträglich als vollständiger Snapshot des
|
|
// Live-Schemas neu exportiert und enthält die trail-Collections seither
|
|
// bereits. Auf einer bestehenden Instanz fällt das nicht auf, weil beide
|
|
// Migrationen längst als angewendet vermerkt sind — auf einer frischen
|
|
// Datenbank laufen sie nacheinander und kollidieren am eindeutigen
|
|
// Collection-Namen. Deshalb der Guard: ist trails schon da, gibt es hier
|
|
// nichts zu tun.
|
|
|
|
migrate((app) => {
|
|
try {
|
|
app.findCollectionByNameOrId('trails')
|
|
return
|
|
} catch {
|
|
// Noch nicht vorhanden — regulär anlegen.
|
|
}
|
|
|
|
const teams = app.findCollectionByNameOrId('teams')
|
|
const users = app.findCollectionByNameOrId('users')
|
|
|
|
// Für jede Collection gleich: Standardfelder, die PocketBase nicht
|
|
// selbst anlegt.
|
|
const timestamps = [
|
|
{ name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
|
|
{ name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
|
|
]
|
|
|
|
const teamScoped = (name) => ({
|
|
name,
|
|
type: 'relation',
|
|
required: true,
|
|
maxSelect: 1,
|
|
collectionId: teams.id,
|
|
cascadeDelete: true,
|
|
})
|
|
|
|
// --- trails (noch ohne current) --------------------------------------
|
|
const trails = new Collection({
|
|
type: 'base',
|
|
name: 'trails',
|
|
listRule: 'team.users.id ?= @request.auth.id',
|
|
viewRule: 'team.users.id ?= @request.auth.id',
|
|
createRule: 'team.users.id ?= @request.auth.id',
|
|
// Bearbeiten dürfen Paten und Team-Admins
|
|
updateRule:
|
|
'stewards.id ?= @request.auth.id' +
|
|
' || team.owner.id ?= @request.auth.id' +
|
|
' || team.admins.id ?= @request.auth.id',
|
|
// Löschen nur Admins — am Trail hängen fremde Marker und Kommentare
|
|
deleteRule:
|
|
'team.owner.id ?= @request.auth.id' +
|
|
' || team.admins.id ?= @request.auth.id',
|
|
fields: [
|
|
teamScoped('team'),
|
|
{ name: 'name', type: 'text', required: true, max: 200 },
|
|
{ name: 'description', type: 'editor' },
|
|
{
|
|
name: 'status',
|
|
type: 'select',
|
|
maxSelect: 1,
|
|
values: ['offen', 'eingeschraenkt', 'gesperrt'],
|
|
},
|
|
{
|
|
name: 'stewards',
|
|
type: 'relation',
|
|
maxSelect: 0,
|
|
collectionId: users.id,
|
|
cascadeDelete: false,
|
|
},
|
|
{
|
|
name: 'event',
|
|
type: 'relation',
|
|
maxSelect: 1,
|
|
collectionId: app.findCollectionByNameOrId('events').id,
|
|
cascadeDelete: false,
|
|
},
|
|
{
|
|
name: 'created_by',
|
|
type: 'relation',
|
|
maxSelect: 1,
|
|
collectionId: users.id,
|
|
cascadeDelete: false,
|
|
},
|
|
...timestamps,
|
|
],
|
|
})
|
|
app.save(trails)
|
|
|
|
// --- trail_versions ---------------------------------------------------
|
|
const versions = new Collection({
|
|
type: 'base',
|
|
name: 'trail_versions',
|
|
listRule: 'trail.team.users.id ?= @request.auth.id',
|
|
viewRule: 'trail.team.users.id ?= @request.auth.id',
|
|
createRule:
|
|
'trail.stewards.id ?= @request.auth.id' +
|
|
' || trail.team.owner.id ?= @request.auth.id' +
|
|
' || trail.team.admins.id ?= @request.auth.id',
|
|
// Versionen sind unveränderlich — eine Korrektur ist eine neue Version
|
|
updateRule: null,
|
|
deleteRule:
|
|
'trail.team.owner.id ?= @request.auth.id' +
|
|
' || trail.team.admins.id ?= @request.auth.id',
|
|
fields: [
|
|
{
|
|
name: 'trail',
|
|
type: 'relation',
|
|
required: true,
|
|
maxSelect: 1,
|
|
collectionId: trails.id,
|
|
cascadeDelete: true,
|
|
},
|
|
{
|
|
name: 'gpx',
|
|
type: 'file',
|
|
maxSelect: 1,
|
|
maxSize: 10485760,
|
|
mimeTypes: ['application/gpx+xml', 'application/xml', 'text/xml'],
|
|
},
|
|
{ name: 'geojson', type: 'json', maxSize: 5000000 },
|
|
{ name: 'elevation', type: 'json', maxSize: 2000000 },
|
|
{ name: 'bounds', type: 'json', maxSize: 1000 },
|
|
{ name: 'distance_m', type: 'number' },
|
|
{ name: 'ascent_m', type: 'number' },
|
|
{ name: 'note', type: 'text', max: 500 },
|
|
{
|
|
name: 'uploaded_by',
|
|
type: 'relation',
|
|
maxSelect: 1,
|
|
collectionId: users.id,
|
|
cascadeDelete: false,
|
|
},
|
|
...timestamps,
|
|
],
|
|
})
|
|
app.save(versions)
|
|
|
|
// --- trails.current nachtragen ---------------------------------------
|
|
// Frisch laden statt das Objekt von oben weiterzuverwenden: das aus
|
|
// `new Collection()` erzeugte Objekt gilt weiterhin als neu, ein zweites
|
|
// app.save() darauf liefe erneut als Insert und scheiterte am
|
|
// eindeutigen Collection-Namen.
|
|
const trailsSaved = app.findCollectionByNameOrId('trails')
|
|
trailsSaved.fields.add(new Field({
|
|
name: 'current',
|
|
type: 'relation',
|
|
maxSelect: 1,
|
|
collectionId: versions.id,
|
|
cascadeDelete: false,
|
|
}))
|
|
app.save(trailsSaved)
|
|
|
|
// --- trail_flags ------------------------------------------------------
|
|
const adminOnly =
|
|
'team.owner.id ?= @request.auth.id' +
|
|
' || team.admins.id ?= @request.auth.id'
|
|
|
|
const flags = new Collection({
|
|
type: 'base',
|
|
name: 'trail_flags',
|
|
listRule: 'team.users.id ?= @request.auth.id',
|
|
viewRule: 'team.users.id ?= @request.auth.id',
|
|
createRule: adminOnly,
|
|
updateRule: adminOnly,
|
|
// Hier bewusst ohne die Marker-Prüfung: trail_markers existiert an
|
|
// dieser Stelle noch nicht, eine @collection.trail_markers-Referenz
|
|
// ließe die Migration auf einer frischen Datenbank scheitern.
|
|
// Die verschärfte Regel setzt 1754500200_trail_flags_delete_rule.js
|
|
// nach, sobald trail_markers angelegt ist.
|
|
deleteRule: adminOnly,
|
|
fields: [
|
|
teamScoped('team'),
|
|
{ name: 'label', type: 'text', required: true, max: 60 },
|
|
{ name: 'icon', type: 'text', max: 40 },
|
|
{ name: 'color', type: 'text', max: 9 },
|
|
{
|
|
name: 'severity',
|
|
type: 'select',
|
|
maxSelect: 1,
|
|
values: ['info', 'warnung', 'kritisch'],
|
|
},
|
|
...timestamps,
|
|
],
|
|
})
|
|
app.save(flags)
|
|
|
|
// --- trail_markers ----------------------------------------------------
|
|
// Anlegen darf jedes Team-Mitglied; ändern nur der Ersteller, Paten
|
|
// oder Admins.
|
|
const ownerOrSteward =
|
|
'created_by.id ?= @request.auth.id' +
|
|
' || trail.stewards.id ?= @request.auth.id' +
|
|
' || team.owner.id ?= @request.auth.id' +
|
|
' || team.admins.id ?= @request.auth.id'
|
|
|
|
const markers = new Collection({
|
|
type: 'base',
|
|
name: 'trail_markers',
|
|
listRule: 'team.users.id ?= @request.auth.id',
|
|
viewRule: 'team.users.id ?= @request.auth.id',
|
|
createRule: 'team.users.id ?= @request.auth.id',
|
|
updateRule: ownerOrSteward,
|
|
deleteRule: ownerOrSteward,
|
|
fields: [
|
|
{
|
|
name: 'trail',
|
|
type: 'relation',
|
|
required: true,
|
|
maxSelect: 1,
|
|
collectionId: trails.id,
|
|
cascadeDelete: true,
|
|
},
|
|
teamScoped('team'),
|
|
{
|
|
name: 'flag',
|
|
type: 'relation',
|
|
required: true,
|
|
maxSelect: 1,
|
|
collectionId: flags.id,
|
|
cascadeDelete: false,
|
|
},
|
|
{ name: 'lat', type: 'number', required: true },
|
|
{ name: 'lng', type: 'number', required: true },
|
|
{ name: 'note', type: 'text', max: 500 },
|
|
{ name: 'resolved', type: 'bool' },
|
|
{
|
|
name: 'created_by',
|
|
type: 'relation',
|
|
maxSelect: 1,
|
|
collectionId: users.id,
|
|
cascadeDelete: false,
|
|
},
|
|
...timestamps,
|
|
],
|
|
})
|
|
app.save(markers)
|
|
|
|
// --- trail_comments ---------------------------------------------------
|
|
const comments = new Collection({
|
|
type: 'base',
|
|
name: 'trail_comments',
|
|
listRule: 'team.users.id ?= @request.auth.id',
|
|
viewRule: 'team.users.id ?= @request.auth.id',
|
|
createRule: 'team.users.id ?= @request.auth.id',
|
|
updateRule: ownerOrSteward,
|
|
deleteRule: ownerOrSteward,
|
|
fields: [
|
|
{
|
|
name: 'trail',
|
|
type: 'relation',
|
|
required: true,
|
|
maxSelect: 1,
|
|
collectionId: trails.id,
|
|
cascadeDelete: true,
|
|
},
|
|
teamScoped('team'),
|
|
{ name: 'text', type: 'text', required: true, max: 2000 },
|
|
{
|
|
name: 'created_by',
|
|
type: 'relation',
|
|
maxSelect: 1,
|
|
collectionId: users.id,
|
|
cascadeDelete: false,
|
|
},
|
|
...timestamps,
|
|
],
|
|
})
|
|
app.save(comments)
|
|
}, (app) => {
|
|
// Rückwärts in umgekehrter Reihenfolge, damit keine Relation ins Leere
|
|
// zeigt. trails zuletzt, weil alle anderen darauf verweisen.
|
|
for (const name of [
|
|
'trail_comments',
|
|
'trail_markers',
|
|
'trail_flags',
|
|
'trail_versions',
|
|
'trails',
|
|
]) {
|
|
try {
|
|
app.delete(app.findCollectionByNameOrId(name))
|
|
} catch {
|
|
// Bereits entfernt — nichts zu tun.
|
|
}
|
|
}
|
|
})
|