/// // Creates the drone-pilot logbook collections: `drones` (the airframes an // operator flies, carrying the classification inputs that drive exemption / // logging-path logic) and `flights` (the logbook entries themselves, modelled on // Denmark's BEK nr. 1649 af 12/12/2023 "Dronebekendtgørelsen" § 5). // // Both collections are reached only through the API Server's superuser service // account (like `organizations` + user management), so their API rules are left // locked (superusers only); the API Server enforces per-role scoping in Go. // // Apply by copying into your PocketBase deployment's `pb_migrations/` directory // and restarting. Written for PocketBase v0.22+/v0.23. Idempotent: each // collection is created only if absent, so re-running is a no-op. // // Depends on 1720300200_add_organizations.js (organizations) and the `users` // auth collection. migrate( (app) => { const orgs = app.findCollectionByNameOrId('organizations') const users = app.findCollectionByNameOrId('users') // ---- drones ----------------------------------------------------------- let drones try { drones = app.findCollectionByNameOrId('drones') } catch (_) { drones = new Collection({ type: 'base', name: 'drones', fields: [ { name: 'name', type: 'text', required: true, max: 120, presentable: true }, { name: 'model', type: 'text', max: 120 }, { name: 'serial', type: 'text', max: 120 }, // Trafikstyrelsen operator number displayed on the drone. { name: 'operator_number', type: 'text', max: 60 }, // Max take-off mass in grams — drives the < 250 g exemption. { name: 'mtom_grams', type: 'number', min: 0 }, { name: 'is_toy', type: 'bool' }, // Has an onboard flight-data recorder (automatic-logging path). { name: 'autologs_flights', type: 'bool' }, // C-class marking: C0..C6 (or blank for legacy/unmarked). { name: 'c_class', type: 'select', maxSelect: 1, values: ['C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6'] }, { name: 'organization', type: 'relation', required: false, collectionId: orgs.id, cascadeDelete: false, minSelect: 0, maxSelect: 1, presentable: false, }, { name: 'created', type: 'autodate', onCreate: true, onUpdate: false }, { name: 'updated', type: 'autodate', onCreate: true, onUpdate: true }, ], indexes: [ 'CREATE INDEX `idx_drones_org` ON `drones` (`organization`)', ], }) app.save(drones) drones = app.findCollectionByNameOrId('drones') } // ---- flights ---------------------------------------------------------- try { app.findCollectionByNameOrId('flights') return // already present } catch (_) { // create below } const flights = new Collection({ type: 'base', name: 'flights', fields: [ // -- BEK 1649 § 5 minimum content -- { name: 'operation_date', type: 'date', required: true }, { name: 'start_time', type: 'text', max: 5 }, // "HH:MM" { name: 'end_time', type: 'text', max: 5 }, // "HH:MM" { name: 'drone', type: 'relation', required: true, collectionId: drones.id, cascadeDelete: false, minSelect: 1, maxSelect: 1, presentable: true, }, // Area flown or route taken (free text; optional GeoJSON alongside). { name: 'area_route', type: 'text', max: 500 }, { name: 'route_geojson', type: 'json', maxSize: 200000 }, // Maximum altitude relative to terrain, in metres AGL. { name: 'max_altitude_agl', type: 'number', min: 0 }, { name: 'remote_pilot', type: 'relation', required: true, collectionId: users.id, cascadeDelete: false, minSelect: 1, maxSelect: 1, presentable: false, }, // Denormalised pilot name — § 5 requires the remote pilot's *name*, which // the users relation alone may not carry. { name: 'pilot_name', type: 'text', max: 160 }, { name: 'certificate_ref', type: 'text', max: 120 }, // -- category / logging path -- { name: 'category', type: 'select', maxSelect: 1, values: ['open', 'specific', 'certified'] }, // Declared flight purpose — drives the exemption computation. { name: 'purpose', type: 'select', maxSelect: 1, values: ['hobby', 'commercial', 'research', 'public', 'club_area'] }, { name: 'logging_path', type: 'select', maxSelect: 1, values: ['automatic', 'manual'] }, // Link to the stored FDR export (automatic path). { name: 'raw_fdr_log_url', type: 'text', max: 500 }, // Authorisation reference for Specific-category ops (STS/PDRA/SORA). { name: 'authorisation_ref', type: 'text', max: 200 }, // -- operational maturity (beyond the legal minimum) -- { name: 'weather', type: 'text', max: 300 }, { name: 'airspace_ref', type: 'text', max: 200 }, { name: 'observer', type: 'text', max: 160 }, { name: 'incidents', type: 'text', max: 1000 }, { name: 'notes', type: 'text', max: 1000 }, // -- ownership + retention -- { name: 'organization', type: 'relation', required: false, collectionId: orgs.id, cascadeDelete: false, minSelect: 0, maxSelect: 1, presentable: false, }, // 5-year retention boundary — computed as operation_date + 5y at create. { name: 'retention_until', type: 'date' }, { name: 'created', type: 'autodate', onCreate: true, onUpdate: false }, { name: 'updated', type: 'autodate', onCreate: true, onUpdate: true }, ], indexes: [ 'CREATE INDEX `idx_flights_pilot` ON `flights` (`remote_pilot`)', 'CREATE INDEX `idx_flights_org` ON `flights` (`organization`)', 'CREATE INDEX `idx_flights_date` ON `flights` (`operation_date`)', ], }) app.save(flights) }, (app) => { // Down: remove flights first (it references drones), then drones. for (const name of ['flights', 'drones']) { try { app.delete(app.findCollectionByNameOrId(name)) } catch (_) { // already gone } } }, )