/// // Benachrichtigungen — eine Mail am Tag, und jeder bestimmt selbst, worüber. // // Zwei Collections, weil zwei verschiedene Dinge gebraucht werden: // // notification_prefs — was will jemand wissen? Ein Datensatz je Konto. // notifications — was ist passiert? Die Warteschlange bis zum Versand. // // Warum eine Warteschlange und nicht sofort eine Mail? Weil sonst ein // Trailkommentar, dem drei Antworten folgen, vier Mails auslöst — und ein // Verein, der sich die nicht leisten kann, das Ganze am dritten Tag // abschaltet. Der Cron-Job in pb_hooks/notifications.pb.js nimmt einmal // täglich alles Unversendete, macht eine Mail daraus und hakt es ab. // // Die Warteschlange ist deshalb kein Zwischenspeicher, sondern der eigentliche // Mechanismus: Der Auslöser schreibt hinein und ist fertig. Ob und wann etwas // verschickt wird, entscheidet allein der Job. Ein Fehler beim Mailversand // bringt damit auch nie das Speichern eines Events durcheinander. // // Wer nichts einstellt, hat trotzdem Voreinstellungen: `notification_prefs` // entsteht erst beim ersten Speichern. Was ohne Datensatz gilt, steht als // DEFAULTS in pb_hooks/notifications_lib.js — an einer Stelle, gültig für // Auslöser und Oberfläche. const OWN_RECORD = '@request.auth.id != "" && user = @request.auth.id' migrate((app) => { const users = app.findCollectionByNameOrId('users') const teams = app.findCollectionByNameOrId('teams') // --- Einstellungen ---------------------------------------------------- // // Alles, was jemand über sich selbst festlegt. Die Regeln lassen nur den // eigenen Datensatz zu — auch beim Anlegen, sonst schriebe jemand // Einstellungen für ein fremdes Konto und bestimmte, was ein anderer // erfährt. try { app.findCollectionByNameOrId('notification_prefs') } catch { const prefs = new Collection({ id: 'pbc_notification_prefs', type: 'base', name: 'notification_prefs', listRule: OWN_RECORD, viewRule: OWN_RECORD, createRule: '@request.auth.id != "" && @request.body.user = @request.auth.id', updateRule: OWN_RECORD, deleteRule: OWN_RECORD, fields: [ { id: 'relation_prefs_user', name: 'user', type: 'relation', required: true, maxSelect: 1, collectionId: users.id, cascadeDelete: true, }, // Hauptschalter. Aus heißt: gar keine Mails, egal was die // einzelnen Schalter sagen. Er steht vor allem für den Fall, // dass jemand für zwei Wochen Ruhe haben will, ohne sich // danach alles neu zusammenzuklicken. { id: 'bool_prefs_enabled', name: 'enabled', type: 'bool' }, { id: 'bool_prefs_event_created', name: 'event_created', type: 'bool' }, { id: 'bool_prefs_event_participation', name: 'event_participation', type: 'bool' }, { id: 'bool_prefs_event_changed', name: 'event_changed', type: 'bool' }, { id: 'bool_prefs_trail_comment', name: 'trail_comment', type: 'bool' }, { id: 'bool_prefs_trail_status', name: 'trail_status', type: 'bool' }, { id: 'bool_prefs_trail_marker', name: 'trail_marker', type: 'bool' }, { id: 'bool_prefs_team_member', name: 'team_member', type: 'bool' }, { id: 'autodate_created', name: 'created', type: 'autodate', onCreate: true, onUpdate: false }, { id: 'autodate_updated', name: 'updated', type: 'autodate', onCreate: true, onUpdate: true }, ], indexes: [ // Ein Datensatz je Konto. Zwei wären zwei Wahrheiten, und // welche gilt, entschiede die Sortierung. 'CREATE UNIQUE INDEX `idx_notification_prefs_user` ON `notification_prefs` (`user`)', ], }) app.save(prefs) } // --- Warteschlange ---------------------------------------------------- // // Lesen darf jeder seine eigenen Einträge — daraus wird später einmal // eine Glocke in der Oberfläche. Schreiben darf über die API niemand: // Alle vier Regeln stehen auf null, was in PocketBase „nur Superuser" // heißt. Gefüllt wird ausschließlich aus den Hooks, und die laufen an den // Regeln vorbei. Sonst schriebe sich jeder selbst Meldungen — oder, // schlimmer, anderen. try { app.findCollectionByNameOrId('notifications') } catch { const queue = new Collection({ id: 'pbc_notifications', type: 'base', name: 'notifications', listRule: OWN_RECORD, viewRule: OWN_RECORD, createRule: null, updateRule: null, deleteRule: null, fields: [ { id: 'relation_notification_user', name: 'user', type: 'relation', required: true, maxSelect: 1, collectionId: users.id, cascadeDelete: true, }, { id: 'relation_notification_team', name: 'team', type: 'relation', required: false, maxSelect: 1, collectionId: teams.id, cascadeDelete: true, }, // Welcher Schalter diesen Eintrag erlaubt hat. Steht mit // drin, damit die Zusammenfassung nach Art gruppieren kann. { id: 'text_notification_type', name: 'type', type: 'text', required: true }, { id: 'text_notification_title', name: 'title', type: 'text', required: true }, { id: 'text_notification_body', name: 'body', type: 'text' }, // Relativer Pfad, kein vollständiger Link: Die Adresse der // App steht in APP_URL und gehört nicht in jeden Datensatz. { id: 'text_notification_link', name: 'link', type: 'text' }, // Leer heißt: wartet auf die nächste Zusammenfassung. { id: 'date_notification_sent', name: 'sent', type: 'date' }, { id: 'autodate_created', name: 'created', type: 'autodate', onCreate: true, onUpdate: false }, { id: 'autodate_updated', name: 'updated', type: 'autodate', onCreate: true, onUpdate: true }, ], indexes: [ // Der Job fragt genau danach: alles Unversendete, nach Konto. 'CREATE INDEX `idx_notifications_pending` ON `notifications` (`user`, `sent`)', ], }) app.save(queue) } }, (app) => { try { app.delete(app.findCollectionByNameOrId('notifications')) } catch { // Schon fort. } try { app.delete(app.findCollectionByNameOrId('notification_prefs')) } catch { // Schon fort. } })