Repository navigation
epic(resources): generic master-data driver — 22 self-describing editable tables behind one endpoint #109
Description
Extends #108, which asked for Bereiche specifically. The endpoint behind it turns out to be a
self-describing registry of 22 editable tables, so the right move is one generic driver, not a
hand-written resource per table.
What the endpoint gives you
POST /index.php?q=churchdb/ajax with func=getMasterData returns data.masterDataTables — every
editable table, with its label, shortname, physical table name, and a full column description
straight out of DESCRIBE (field, type, null, key, default, extra). Captured against eqrm prod
(CT 3.135.2, HAR 2026-08-13):
| Label | shortname | table | cols |
|---|---|---|---|
| Personenstatus | status |
cdb_status |
7 |
| Standort | station |
cdb_station |
24 |
| Bereich | dep |
cdb_bereich |
4 |
| Gruppentypen | groupTypes |
cdb_gruppentyp |
14 |
| Gruppentyp-Rolle | grouptypeMemberstatus |
cdb_group_type_role |
12 |
| Sicherheitslevel | securitylevel |
cc_securitylevel |
3 |
| Kommentare-Viewer | comment_viewer |
cdb_comment_viewer |
3 |
| Personenfelder | feld |
cdb_feld |
20 |
| Kategorie | groupcategory |
cdb_groupcategory |
8 |
| Altersgruppen | agegroups |
cdb_agegroups |
5 |
| Zielgruppen | targetgroup |
cdb_targetgroup |
3 |
| growpath | growpath |
cdb_growpath |
7 |
| Geschlecht, Familienstand, Beziehungstyp, Nationalität, Kontakt-Label, days, meeting_rhythmus, Gruppentreffen-Template, privacy-policy ×2 | 2–9 |
Writes go to the same endpoint (create = empty id, update = an id):
func=saveMasterData&table=cdb_bereich&id=&col0=bezeichnung&value0=…&col1=kuerzel&value1=…&col2=sortkey&value2=…
→ {"status":"success","data":null}
Because the schema is served by the instance, a generic driver needs no per-table knowledge: read
masterDataTables, validate declared columns against desc, emit colN/valueN pairs.
Three things this unblocks that are currently recorded as impossible
#96 — person statuses. The conclusion was that statuses are "neither declarable nor adoptable"
because /statuses is a read-only REST catalog. cdb_status is in this registry, fully editable
(bezeichnung, kuerzel, mitglied_yn, securitylevel_id, sortkey). The blocker was the REST
API, not ChurchTools.
#102 — cdb_comment_viewer-blocked grants. Three role instances on Bereich Pastoral Care are
blocked by exactly one cdb_comment_viewer-scoped grant each. That dimension is master data with
names (cdb_comment_viewer: id, bezeichnung, sortkey), so it can become a resolvable ref kind —
turning those from "unmanageable" into "declarable".
A latent assumption worth re-examining: cc_securitylevel is editable master data. ct-cli and
eqrm/ct-structure both treat security-level dataIds 1/2/3 as numeric-universal — safe to hard-code
because they "mean the same thing on every instance". If security levels are admin-editable rows with
a sortkey, that holds only by convention. Someone adding or reordering a level on one host silently
changes what a hard-coded scope: [1, 2, 3] grants there. At minimum this deserves a documented
check; possibly security levels should become refs like everything else.
Design notes
- Read through REST where it exists. Campuses (
/campuses), group types (/group/grouptypes),
age groups, roles already have REST resources. Two write paths to the same row would be a mess —
either keep REST authoritative for those tables and use master-data only for the ones REST cannot
write, or migrate deliberately. My preference: master-data driver covers only tables with no REST
write, and the registry is used to discover which those are. - Blast radius is instance-wide. These rows are referenced by every person and group; a botched
bezeichnungupdate renames a status everywhere. Same protections as elsewhere: apply never
deletes,preventDestroyhonoured, and a plan that renders master-data changes distinctly. - Undocumented endpoint. Treat like the other reverse-engineered surfaces: record the capture,
re-probe on CT major bumps, and fail loudly ifmasterDataTableslacks a declared table or column
rather than posting an unknowncolN. func=deleteMasterDatawas not captured — unknown whether it exists. Not needed for apply.
Acceptance
- A generic master-data resource driver reads
masterDataTablesand validates declared columns
against the instance's owndesc. - Tables with no REST write path (Bereich, Personenstatus, comment_viewer, …) are declarable,
adoptable and applied. - Declaring a column the instance does not report is a hard error, not a silent POST.
- feat(resources): person statuses are not declarable — the
statuspermission domain is only half-portable #96 and thecdb_comment_viewerhalf of feat(permissions): opt-in partial grant ownership — one module-scoped grant currently blocks 403 of 590 grants #102 are re-evaluated against this mechanism. - The
cc_securitylevel-ids-are-universal assumption is either verified or replaced with refs.
Related: #108 (Bereiche, the case that found this), #96, #102, #105 (same endpoint family — the
permission catalog is generated from churchauth/ajax func=getMasterData), #98.
Activity
feat(permissions): commentViewer scope refs — the cdb_comment_viewer …
feat(resources): Bereiche are a managed resource, written through the…
2000game commentedon Aug 14, 2026
Probed live — the premise does not hold, so closing in favour of the narrow #108
Ran
getMasterDataagainst eqrm-dev, CT 3.135.2, 2026-08-14 and classified all 24 registry tables against that instance's own OpenAPI spec. The epic assumed most of these tables lack a REST write path. On a live instance, most of them have one.Under the standing rule "if a feature has a REST API, use the REST API", a generic master-data driver would own exactly one table inside this tool's mandate.
REST write path exists → REST stays authoritative (15)
cdb_station/campusescdb_gruppentyp/group/grouptypescdb_status/statusescc_securitylevel/securitylevels/{id}— POST/PATCH/DELETEcdb_comment_viewer/person/commentviewerscdb_groupcategory/group/groupcategoriescdb_group_type_role/group/rolescdb_agegroups/group/agegroupscdb_targetgroup/group/targetgroupscdb_gruppentreffen_template/group/meetingtemplatescdb_feld/dbfieldscdb_geschlecht/person/sexescdb_beziehungstyp/person/relationshiptypescdb_growpath/person/growpathscdb_contact_labels/contactlabelsREST read-only (1)
cdb_bereich→GET /departments, no write verb, no/departments/{id}at all.No REST endpoint (8)
cdb_familienstand,cdb_nationalitaet,cdb_days,cdb_custom_choice,cdb_custom_multiselct,cdb_custom_selection,cdb_privacy_policy_agreement_types,cdb_privacy_policy_agreement_who— person master data and field option lists, all outside the structural mandate.What shipped instead (PR #112)
departmentresource backed bysaveMasterData, with the registry used only to validate its columns against the instance's ownDESCRIBE. Verified end to end on dev with the real CLI: plan → apply → re-plan no-op → update → delete, instance left as found.deleteMasterDatadoes exist (a made-updelMasterDataanswers"was not defined as Function!", so the endpoint validates function names and the success is real).cc_securitylevelitem — resolved the other way./securitylevels/{id}has POST/PATCH/DELETE, so security levels became a managed REST resource (bug(permissions): "security-level ids are universal" is an assumption, not a guarantee — they are editable master data #110), not a master-data one. The id is caller-assigned (you POST to the id you want; CT 409s if taken), which needed a small engine addition and, incidentally, makes numericscope: [1, 2, 3]genuinely portable for a config that owns its levels.cdb_comment_viewerhalf of feat(permissions): opt-in partial grant ownership — one module-scoped grant currently blocks 403 of 590 grants #102 —/person/commentviewersis plain REST CRUD, so it became a{ commentViewer: "…" }scope ref. No legacy endpoint needed. Noted on feat(permissions): opt-in partial grant ownership — one module-scoped grant currently blocks 403 of 590 grants #102.statuspermission domain is only half-portable #96 needs no change —/statusesis full REST CRUD and person statuses are already adoptable. REST wins; the master-data route would be a second write path to the same rows.What survives from this epic
The audit lesson, which is #111's subject and shipped there: a "no write API" verdict now requires a REST probe and a
getMasterData → masterDataTablesprobe, plus a UI capture where neither settles it. That is what turned three "impossible" claims into three shipped features.Closing as overtaken by the probe. If the mandate ever widens to person master data (marital status, nationality, field option lists), the generic driver becomes worth building and this comment is the inventory to start from.