/// // 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. migrate((app) => { 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 --------------------------------------- trails.fields.add(new Field({ name: 'current', type: 'relation', maxSelect: 1, collectionId: versions.id, cascadeDelete: false, })) app.save(trails) // --- 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, // Löschen nur, solange kein Marker diesen Typ verwendet — sonst // bliebe ein Pflichtfeld zurück, das ins Leere zeigt. deleteRule: adminOnly + ' && @collection.trail_markers.flag ?!= id', 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. } } })