De backend
Naast de frontend kan een module server-side code hebben: TypeScript dat Tabbr in een sandbox op de server uitvoert. Daar hoort werk thuis dat niet van een open browser mag afhangen: ’s nachts statussen bijwerken, slimme groepen vullen, synchroniseren met een externe dienst, rollen toekennen.
Een eerste functie
Section titled “Een eerste functie”De backend is een gewoon TypeScript-bestand dat functies exporteert. Het manifest zegt wanneer Tabbr ze oproept.
/** Elke nacht: de status van elke bevoegdheid (gepland, geldig, vervallen) volgens haar datums. */export function refreshAll(): void { const all = tabbr.entities.authorisation.query({ limit: 5000 }); for (const record of all) { const status = statusOf(record); if (status !== record.status) { tabbr.entities.authorisation.updateFields(record.id, { status }); } } tabbr.settings.set('lastRun', { at: new Date().toISOString(), count: all.length });}
/** Na het bewaren van een bevoegdheid: alleen die ene bijwerken. */export function onAuthorisationSaved(authorisation: { id: string }): void { const record = tabbr.entities.authorisation.find(authorisation.id); if (record) refreshStatuses([record]);}"backend": { "entry": "backend/src/index.ts", "triggers": [ { "type": "CRON", "name": "refreshAll", "cronExpression": "10 2 * * *", "handler": "refreshAll" }, { "type": "ENTITY_HOOK", "name": "authorisationSaved", "entity": "authorisation", "event": "onCreated", "handler": "onAuthorisationSaved" }, { "type": "ENTITY_HOOK", "name": "authorisationUpdated", "entity": "authorisation", "event": "onUpdated", "handler": "onAuthorisationSaved" } ]}Triggers
Section titled “Triggers”| Type | Wanneer |
|---|---|
CRON |
Volgens cronExpression (5 velden, bv. 10 2 * * * = elke nacht om 02:10). |
ENTITY_HOOK, event onCreate / onUpdate / onDelete |
Vóór het bewaren van een record, in dezelfde transactie. |
ENTITY_HOOK, event onCreated / onUpdated |
Na de commit, op de achtergrond. |
- De schrijfacties van je module zelf starten haar eigen handlers niet opnieuw: geen oneindige lussen.
- Een trigger met de hand starten kan met
POST /api/custommodule/{moduleId}/trigger/{name}, of vanuit je frontend metinjectModuleApi(moduleId).trigger(name)— handig voor een knop Nu synchroniseren.
Wat je in de sandbox hebt
Section titled “Wat je in de sandbox hebt”Gegevens: tabbr.entities
Section titled “Gegevens: tabbr.entities”Elke entiteit van de club, bij naam: tabbr.entities.person, tabbr.entities.flight,
tabbr.entities.groupMember, … Lezen kan op elke entiteit; schrijven op de entiteiten van je eigen
module en op flight.
| Methode | Wat ze doet |
|---|---|
find(id) |
Eén record, of null. |
findByFieldEquals(veld, waarde) |
De records met die waarde. |
query({ where, filterLines, dataList, q, limit, offset }) |
Records — standaard 500, maximaal 5000 per oproep; blader met offset. |
queryIds(…) |
Alleen de id’s (standaard allemaal). |
create(velden) |
Maakt een record, geeft { id }. |
updateFields(id, velden) |
Past velden aan. |
delete(id) |
Verwijdert een record. |
// de lopende lidmaatschappen van een persoonconst memberships = tabbr.entities.groupMember.query({ where: [ { field: 'person', value: personId }, { field: 'until', operator: 'IS_EMPTY' }, ],});
// de filter van een lijst van de club hergebruikenconst actief = tabbr.entities.person.queryIds({ dataList: 'Clubleden' });Een record is een gewoon object: id, $label (hoe het record heet) en één eigenschap per veld. Een veld
dat je module declareert, heet naar zijn key — stabiel, ook als de club het hernoemt; de eigen velden van de
club naar hun naam, ingebouwde velden naar hun eigenschap (bv. firstname). Een verwijzing schrijf je als
{ id }.
Instellingen: tabbr.settings
Section titled “Instellingen: tabbr.settings”tabbr.settings.get(key) en tabbr.settings.set(key, value) bewaren de instellingen van je module in de club.
Een instelling die in secretFieldNames staat (een API-key), kan je server-side lezen maar komt nooit
terug naar de browser.
Externe API’s: http
Section titled “Externe API’s: http”const apiKey = tabbr.settings.get<string>('apiKey');const res = http.fetchJson<Flight[]>('https://admin.gliding.app/api/flights.json', { method: 'GET', headers: { 'X-API-KEY': apiKey! },});if (!res.ok) throw new Error(`ZweefApp antwoordde ${res.status}`);for (const flight of res.body) { /* … */ }http.fetchJson werkt alleen voor de hosts in allowedHosts van je manifest. Alles daarbuiten wordt
geweigerd.
Contracten: tabbr.uses
Section titled “Contracten: tabbr.uses”De contracten uit uses en optionalUses, bij naam — zie Contracten:
const instructeurs = tabbr.uses['sys.access'].personsWithRole('INSTRUCTEUR');const akkoord = tabbr.uses['tabbr.consent']?.personsWith('zweefapp.share') ?? [];Testen
Section titled “Testen”Houd logica die frontend en backend delen in shared/, en test ze met de ingebouwde testrunner van Node:
import { test } from 'node:test';import assert from 'node:assert/strict';import { statusOf } from '../../shared/authorisations.js';
test('een bevoegdheid met een einddatum in het verleden is vervallen', () => { assert.equal(statusOf({ from: '2024-01-01', until: '2025-01-01' }, '2026-03-01'), 'expired');});npm test # node --test backend/test/*.test.mjsnpm run typecheck # tsc -p backend/tsconfig.jsonDe Module Store voert test en typecheck uit bij elke ingediende versie: faalt er een, dan wordt de versie
niet gebouwd.