From 6d84d162bda1da2ba8a8b0107d31278a86dd463b Mon Sep 17 00:00:00 2001 From: Felix Kotschenreuther Date: Sat, 26 Sep 2026 12:37:03 +0200 Subject: [PATCH 1/2] docs: correct four known errors (group-status catalog, /dbfields writes, group-type change, provider link) - Group statuses DO have a read catalog: GET /person/masterdata -> groupStatuses (1 active, 2 pending, 3 archived, 4 finished; identical on eqrm prod and dev). #67 missed it; ct still does not resolve by name (#157). - /dbfields: the prod OpenAPI spec now lists POST/PUT/DELETE, including options. Not live-tested; ct keeps treating field definitions as read-only. - groupTypeId cannot change through the regular group update. It needs POST /groups/{id}/grouptype with a role mapping (#171). - README: the provider repo is public and released. --- README.md | 5 +++-- docs/api-coverage.md | 36 +++++++++++++++--------------- docs/configuration.md | 4 +++- docs/group-field-decisions.md | 2 +- docs/handbuch/dynamic-groups.md | 4 ++-- docs/handbuch/field-definitions.md | 21 +++++++++++------ docs/runbook-manual-surface.md | 20 ++++++++--------- 7 files changed, 51 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 19de06e..06f921f 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ code, and reconcile it against the ChurchTools API with Terraform-style > **Frozen.** The TypeScript config DSL receives bugfixes only and is removed > in ct-cli 5.0. Its successor is `terraform-provider-churchtools`, an > OpenTofu/Terraform provider that replaces the state file with tfstate and the -> logical-key resolver with native resource references. Its repository is not -> public yet; this note will link it at the provider's first release. +> logical-key resolver with native resource references: +> [eqrm/terraform-provider-churchtools](https://github.com/eqrm/terraform-provider-churchtools) +> (public; first releases 2026-09). > > Migrating needs no re-adoption for the resource types the provider already > covers: `ct export tf` generates HCL plus `import` blocks from your existing diff --git a/docs/api-coverage.md b/docs/api-coverage.md index d85a98c..8246a8f 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -15,22 +15,22 @@ Analysis of the ChurchTools OpenAPI spec (`openapi.json`, OpenAPI 3.1.0, 487 pat Methods marked only if they actually exist on the matched path. "Update" = PUT or PATCH (noted). Collection paths (list/create) vs item paths (`/{id}`) are separated. -| # | Resource | Matched path(s) | GET (list / by-id) | POST (create) | PUT/PATCH (update) | DELETE | Verdict | -| --- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------- | ----------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | `ct_campus` | `/campuses`, `/campuses/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | -| 2 | `ct_group_type` | `/group/grouptypes`, `/group/grouptypes/{groupTypeId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | -| 3 | `ct_group` | `/groups`, `/groups/{groupId}` | list ✅ / by-id ✅ | ✅ | ✅ PATCH | ✅ | **Full CRUD** | -| 4 | `ct_group_hierarchy` | `/groups/hierarchies` (GET), `/groups/{groupId}/children` (GET), `/groups/{groupId}/parents` (GET), `/groups/{groupId}/parents/{parentGroupId}` (PUT/DELETE) | list ✅ (hierarchies/children/parents) | — (no collection POST) | ✅ PUT links a parent | ✅ unlinks a parent | **Writable** — parent/child edges created & removed via PUT/DELETE on the item path (no POST needed) | -| 5 | `ct_group_role` | `/group/roles`, `/group/roles/{roleId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** (master-data roles). Per-group assignment lives separately at `/groups/{groupId}/roles` GET + `/groups/{groupId}/roles/{roleId}` PATCH. | -| 6 | `ct_dynamic_group` | `/dynamicgroups` (GET), `/dynamicgroups/{groupId}/ruleset` (GET/PUT/DELETE), `/dynamicgroups/{groupId}/status` (GET/PUT), `/dynamicgroups/refresh` & `/dynamicgroups/{groupId}/refresh` (POST) | list ✅ / ruleset & status by-id ✅ | ⚠️ POST only triggers _refresh_, not create | ✅ PUT ruleset & status | ✅ ruleset DELETE | **Partial** — ruleset is fully updatable/deletable; the group entity itself is created via `/groups` (POST) then given a ruleset. No dedicated create/delete of the dynamic-group record. | -| 7 | `ct_permission` | `/permissions/global` (GET), `/permissions/{domainType}` (GET), `/permissions/{domainType}/{domainId}` (GET/PUT/DELETE); `/permissions/internal/...` (GET) | list ✅ / by-id ✅ | — (no collection POST) | ✅ PUT sets permission | ✅ | **Writable** — assign/revoke via PUT/DELETE on `/{domainType}/{domainId}` | -| 8 | `ct_group_status` | none — see note | ❌ | ❌ | ❌ | ❌ | **No REST endpoint at all → manual, permanently (#67).** `/group/memberstatus` looks like a match but is a DIFFERENT dimension — **member** statuses (`{id: "active", name: "Active"}`, STRING ids, assignable per-membership), not group statuses (`groupStatusId`, numeric, e.g. 1 = active / 4 = archived). Live-verified 2026-07-10 on eqrm prod: `/groups/statuses` parses as `/groups/{groupId}` (400), `/group/statuses` and `/groupstatuses` both 404 — no read OR write endpoint for group statuses exists. (`/statuses` + `/statuses/{id}` DO offer full CRUD, but that is the person/community **Status** master data, tag `Status`, a third, unrelated dimension — do not conflate any of the three.) `groupStatusId` stays a plain numeric field on `ct_group`, authored directly — never resolved by name. | -| 9 | `ct_age_group` | `/group/agegroups`, `/group/agegroups/{ageGroupId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | -| 10 | `ct_target_group` | `/group/targetgroups`, `/group/targetgroups/{targetGroupId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | -| 11 | `ct_meeting_point` | — none — | ❌ | ❌ | ❌ | ❌ | **Not in API → fully manual.** Zero matches for `treffpunkt`/`meetingpoint`/`meeting point` anywhere in the spec. Closest neighbours are _meeting templates_ (`/group/meetingtemplates`, full CRUD) and _group meetings_ (`/groups/{groupId}/meetings`, CRUD) — different concepts; confirm with product whether "meeting point" was meant to be one of those. | -| 12 | `ct_relationship_type` | `/person/relationshiptypes`, `/person/relationshiptypes/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | -| 13 | `ct_person_status` | `/statuses`, `/statuses/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD, live-verified 2026-08-13** (eqrm prod, CT 3.135.2, read from the instance OpenAPI spec): `/statuses` → GET, POST; `/statuses/{id}` → GET, PUT, DELETE. `POST` requires `name`, `shorty`, `isMember`; `PUT` requires ALL of `name`, `shorty`, `isMember`, `isSearchable`, `sortKey`, `securityLevelId` — uniquely strict among managed types (every other managed PUT declares no required fields), and since PUT is a full replace the registry manages all six rather than a subset. The person/community **Status** master data (tag `Status`): "0 - First", "3 - Group Active", …, the domain a `ct.status` permission grant hangs off. Adoptable since #96, which is what makes a config using that domain self-sufficient across hosts. Do NOT conflate with `ct_group_status` (row 8, no endpoint at all) or `/group/memberstatus` (member statuses, string ids). Master data — never a person record; the people guard is unaffected. | -| 14 | `ct_department` | `/departments` | list ✅ / by-id ❌ | ❌ | ❌ | ❌ | **No REST write path — a ref catalog here, not a managed resource.** Live-probed 2026-08-13 (eqrm prod, CT 3.135.2): `GET /departments` returns `[{id, name, nameTranslated, sortKey, shorty}]`; no `POST`/`PUT`/`DELETE` on `/departments` exists in the spec, and there is no `/departments/{id}` path at all. Bereiche are the `cdb_bereich` permission scope dimension (`churchdb:view alldata`), so `ct` resolves them BY NAME for a `scope: [{ department: "…" }]` reference (#98) and surfaces them via `ct get departments` — but `ct.department` declares one and `ct adopt department ` adopts one. **Managed since #108** — `ct` writes Bereiche through the legacy `POST /index.php?q=churchdb/ajax` `func=saveMasterData` interface the admin UI uses (create/update/delete all verified live on eqrm-dev 2026-08-14). It appears in no OpenAPI spec, which is why an OpenAPI-only audit could never have found it (#111). Note there is no `/departments/{id}` at all, so a single Bereich is read by filtering the collection. | +| # | Resource | Matched path(s) | GET (list / by-id) | POST (create) | PUT/PATCH (update) | DELETE | Verdict | +| --- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------- | ----------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1 | `ct_campus` | `/campuses`, `/campuses/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | +| 2 | `ct_group_type` | `/group/grouptypes`, `/group/grouptypes/{groupTypeId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | +| 3 | `ct_group` | `/groups`, `/groups/{groupId}` | list ✅ / by-id ✅ | ✅ | ✅ PATCH | ✅ | **Full CRUD** | +| 4 | `ct_group_hierarchy` | `/groups/hierarchies` (GET), `/groups/{groupId}/children` (GET), `/groups/{groupId}/parents` (GET), `/groups/{groupId}/parents/{parentGroupId}` (PUT/DELETE) | list ✅ (hierarchies/children/parents) | — (no collection POST) | ✅ PUT links a parent | ✅ unlinks a parent | **Writable** — parent/child edges created & removed via PUT/DELETE on the item path (no POST needed) | +| 5 | `ct_group_role` | `/group/roles`, `/group/roles/{roleId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** (master-data roles). Per-group assignment lives separately at `/groups/{groupId}/roles` GET + `/groups/{groupId}/roles/{roleId}` PATCH. | +| 6 | `ct_dynamic_group` | `/dynamicgroups` (GET), `/dynamicgroups/{groupId}/ruleset` (GET/PUT/DELETE), `/dynamicgroups/{groupId}/status` (GET/PUT), `/dynamicgroups/refresh` & `/dynamicgroups/{groupId}/refresh` (POST) | list ✅ / ruleset & status by-id ✅ | ⚠️ POST only triggers _refresh_, not create | ✅ PUT ruleset & status | ✅ ruleset DELETE | **Partial** — ruleset is fully updatable/deletable; the group entity itself is created via `/groups` (POST) then given a ruleset. No dedicated create/delete of the dynamic-group record. | +| 7 | `ct_permission` | `/permissions/global` (GET), `/permissions/{domainType}` (GET), `/permissions/{domainType}/{domainId}` (GET/PUT/DELETE); `/permissions/internal/...` (GET) | list ✅ / by-id ✅ | — (no collection POST) | ✅ PUT sets permission | ✅ | **Writable** — assign/revoke via PUT/DELETE on `/{domainType}/{domainId}` | +| 8 | `ct_group_status` | none — see note | ❌ | ❌ | ❌ | ❌ | **Read catalog: `GET /person/masterdata` → `groupStatuses`** (#157, verified live 2026-08-27 and 2026-09-26: 1 `active`, 2 `pending`, 3 `archived`, 4 `finished`, identical on eqrm prod and dev). **No write endpoint.** The 2026-07-10 audit below looked for a dedicated path and missed the masterdata catalog; ct does not resolve group statuses by name yet (#157). `/group/memberstatus` looks like a match but is a DIFFERENT dimension — **member** statuses (`{id: "active", name: "Active"}`, STRING ids, assignable per-membership), not group statuses (`groupStatusId`, numeric: 1 active / 2 pending / 3 archived / 4 finished). Live-verified 2026-07-10 on eqrm prod: `/groups/statuses` parses as `/groups/{groupId}` (400), `/group/statuses` and `/groupstatuses` both 404 — no dedicated read or write path exists — but see the masterdata catalog above. (`/statuses` + `/statuses/{id}` DO offer full CRUD, but that is the person/community **Status** master data, tag `Status`, a third, unrelated dimension — do not conflate any of the three.) `groupStatusId` stays a plain numeric field on `ct_group`, authored directly — never resolved by name. | +| 9 | `ct_age_group` | `/group/agegroups`, `/group/agegroups/{ageGroupId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | +| 10 | `ct_target_group` | `/group/targetgroups`, `/group/targetgroups/{targetGroupId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | +| 11 | `ct_meeting_point` | — none — | ❌ | ❌ | ❌ | ❌ | **Not in API → fully manual.** Zero matches for `treffpunkt`/`meetingpoint`/`meeting point` anywhere in the spec. Closest neighbours are _meeting templates_ (`/group/meetingtemplates`, full CRUD) and _group meetings_ (`/groups/{groupId}/meetings`, CRUD) — different concepts; confirm with product whether "meeting point" was meant to be one of those. | +| 12 | `ct_relationship_type` | `/person/relationshiptypes`, `/person/relationshiptypes/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | +| 13 | `ct_person_status` | `/statuses`, `/statuses/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD, live-verified 2026-08-13** (eqrm prod, CT 3.135.2, read from the instance OpenAPI spec): `/statuses` → GET, POST; `/statuses/{id}` → GET, PUT, DELETE. `POST` requires `name`, `shorty`, `isMember`; `PUT` requires ALL of `name`, `shorty`, `isMember`, `isSearchable`, `sortKey`, `securityLevelId` — uniquely strict among managed types (every other managed PUT declares no required fields), and since PUT is a full replace the registry manages all six rather than a subset. The person/community **Status** master data (tag `Status`): "0 - First", "3 - Group Active", …, the domain a `ct.status` permission grant hangs off. Adoptable since #96, which is what makes a config using that domain self-sufficient across hosts. Do NOT conflate with `ct_group_status` (row 8, no endpoint at all) or `/group/memberstatus` (member statuses, string ids). Master data — never a person record; the people guard is unaffected. | +| 14 | `ct_department` | `/departments` | list ✅ / by-id ❌ | ❌ | ❌ | ❌ | **No REST write path — a ref catalog here, not a managed resource.** Live-probed 2026-08-13 (eqrm prod, CT 3.135.2): `GET /departments` returns `[{id, name, nameTranslated, sortKey, shorty}]`; no `POST`/`PUT`/`DELETE` on `/departments` exists in the spec, and there is no `/departments/{id}` path at all. Bereiche are the `cdb_bereich` permission scope dimension (`churchdb:view alldata`), so `ct` resolves them BY NAME for a `scope: [{ department: "…" }]` reference (#98) and surfaces them via `ct get departments` — but `ct.department` declares one and `ct adopt department ` adopts one. **Managed since #108** — `ct` writes Bereiche through the legacy `POST /index.php?q=churchdb/ajax` `func=saveMasterData` interface the admin UI uses (create/update/delete all verified live on eqrm-dev 2026-08-14). It appears in no OpenAPI spec, which is why an OpenAPI-only audit could never have found it (#111). Note there is no `/departments/{id}` at all, so a single Bereich is read by filtering the collection. | | 15 | `ct_group_member_field` | `/groups/{groupId}/memberfields` (GET), `/groups/{groupId}/memberfields/group` (POST), `/groups/{groupId}/memberfields/group/{groupMemberFieldId}` (PATCH/PUT/DELETE) | list ✅ (per group) / by-id ❌ | ✅ | ✅ PATCH (PUT fallback) | ✅ | **Full CRUD, but GROUP-SCOPED — not a standalone resource (#135).** A member field belongs to exactly one group and is not globally reusable, so it has no collection path of its own and no entry in the resource registry: it is a _synthetic owned sub-resource_ of `ct_group`, declared inside `ct.group({ memberFields: [...] })`, exactly like hierarchy `parents` and the dynamic ruleset. Its portable identity is the managed group key plus a local field key (`ojbp_2026_27_praktikum_1::wahl`); a ChurchTools field id never reaches authored config or an adopted blueprint. Reads are narrowed to rows the `/memberfields/group` endpoints own — the same GET also returns fields sourced from person master data / group-type defaults, which are neither adoptable nor writable here. `apply` NEVER deletes one (a field dropped from config produces no desired diff key at all); removal is the explicit `ct destroy --member-field ::`. Update uses `PATCH` (partial, so unmanaged siblings survive) and falls back to `PUT` on a 405/501. Definitions only — never a per-person value; `assertNotPeople` still guards every path. Distinct from the group CUSTOM fields of #48/#60 (`/dbfields`, `fieldCategory.table == "cdb_gruppe"`), which describe the group RECORD rather than what its members are asked. | @@ -71,7 +71,7 @@ No collection POST; state is set/removed through PUT/DELETE on the item path. Mo ### Read-only / not in API → keep manual for now — 2 resources -- `ct_group_status` — **no REST endpoint at all, read or write** (#67; corrected 2026-07-10 — a prior version of this table wrongly matched `GET /group/memberstatus`, which is actually **member** statuses, a different dimension with string ids). `groupStatusId` remains a plain numeric field. (Do not substitute `/statuses` either — that's person-status master data, a third dimension.) +- `ct_group_status` — **read-only catalog in `GET /person/masterdata` → `groupStatuses`** (#157), no write endpoint. (#67's "no REST endpoint at all" missed that catalog; its other correction stands: `GET /group/memberstatus` is **member** statuses, a different dimension with string ids.) `groupStatusId` remains a plain numeric field. (Do not substitute `/statuses` either — that's person-status master data, a third dimension.) - `ct_meeting_point` — **no endpoint at all**; cannot be automated until CT ships one (or until "meeting point" is redefined onto meeting-templates/meetings, both of which are full CRUD). ### Version gate @@ -91,7 +91,7 @@ the runbook's re-audit procedure once the schema is regenerated. | Resource | Matched path(s) | GET | POST/PUT/PATCH/DELETE | Verdict | | ------------------------------------ | ------------------------------------------------------- | ------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Person master-data model | `/person/masterdata` | ✅ (single object) | ❌ | **Read-only.** Versionable master-data model incl. the `securityLevels` enumeration. No write endpoint (edited in the CT master-data admin UI). | -| Data-field definitions (Datenfelder) | `/dbfields`, `/dbfields/{id}` | list ✅ / by-id ✅ | ❌ | **Read-only.** Unified person + group field definitions, discriminated by `fieldCategory`. Mutation only via legacy churchdb AJAX (`db_insert/update/deletefields`), not REST. | +| Data-field definitions (Datenfelder) | `/dbfields`, `/dbfields/{id}` | list ✅ / by-id ✅ | ❌ | **ct treats it as read-only.** Unified person + group field definitions, discriminated by `fieldCategory`. **The eqrm prod OpenAPI spec (read 2026-09-26) now lists REST writes:** `POST /dbfields`, `PUT`/`DELETE /dbfields/{id}`, and `POST`/`PUT`/`DELETE` on `/dbfields/{id}/options`. Not live-tested; see `docs/handbuch/field-definitions.md`. | | Security levels | `/securitylevels`, `/securitylevels/{id}` | list ✅ / by-id ✅ | ✅ POST, PATCH, DELETE | **Full CRUD — but not on the usual paths.** Live-probed 2026-08-14 (eqrm-dev, CT 3.135.2): `/securitylevels` is GET-only, and **create is `POST /securitylevels/{id}`** (body `{name}`), update is `PATCH /securitylevels/{id}` (body `{name, newid, forcereorder}` — reordering is first-class), delete is `DELETE /securitylevels/{id}`. **Managed since #110** — the registry gained a `createPath` hook and a `callerAssignedId` flag for it; `ct.securityLevel({ key, id, name })` declares one, `ct adopt security-level ` adopts one. Changing a declared id is refused at plan time (that is a renumber, which rewrites what every numeric `cc_securitylevel` scope means). Resolvable by name as a scope ref since #110. The ids are NOT protocol constants — `cc_securitylevel` is an editable master-data table with an auto-increment id. | | Comment viewers (Kommentare-Viewer) | `/person/commentviewers`, `/person/commentviewers/{id}` | list ✅ / by-id ✅ | ✅ POST, PUT, DELETE | **Full CRUD on conventional paths.** Fully live-probed 2026-08-26 (eqrm-dev, CT 3.135.2 — one throwaway row created, read, updated and deleted): `GET /person/commentviewers` returns a flat `[{id, name, nameTranslated, sortKey}]` (`id: 0` — "Alle" — is a real, built-in row), **create is a plain `POST /person/commentviewers`** with CT minting the id, and the item path takes GET/PUT/DELETE (an absent id gives a clean 404 `error.notfound`). `nameTranslated` is derived from `name`, not independently writable. **Managed since #151** — `ct.commentViewer({ key, name, sortKey })` declares one, `ct adopt comment-viewer ` adopts one; no registry machinery was needed (CT mints the id, and the writes are REST). `cdb_comment_viewer` is the scope dimension of `churchdb:view comments`, and it was the last one a config could not express portably: the ids differ across hosts of the same deployment, and before #151 a config had only the raw numeric `dataId`. The minted id is an auto-increment that does not reuse deleted rows, so it can never be caller-assigned. | diff --git a/docs/configuration.md b/docs/configuration.md index 7bb8cb0..21a430c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,7 +68,9 @@ where it was used. Raw numeric ids remain a valid escape hatch everywhere; see for a zero-numeric-id config. **Exception: `groupStatusId` (a group's lifecycle status) is numeric-only, -always.** ChurchTools exposes no REST catalog to resolve a status by name +always.** ChurchTools does expose a read catalog (`GET /person/masterdata` → +`groupStatuses`: 1 active, 2 pending, 3 archived, 4 finished), but ct does not resolve +it by name yet (#157) — so a status still cannot be resolved by name (`/group/memberstatus` is a different dimension, member statuses; #67), so a `status:` declaration fails fast at eval time rather than resolving against the wrong dimension. diff --git a/docs/group-field-decisions.md b/docs/group-field-decisions.md index 0a3b63f..1129e32 100644 --- a/docs/group-field-decisions.md +++ b/docs/group-field-decisions.md @@ -26,7 +26,7 @@ field — a plain top-level key. `campusId` is wired the same deliberate way as | Field | Decision | Rationale | | ---------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | **managed** | Core identity; already managed. | -| `groupTypeId` | **managed** | Determines the group's kind and role template; already managed. | +| `groupTypeId` | **managed** | Determines the group's kind and role template; already managed. **Changing it on an existing group is not a normal update:** ChurchTools rejects `groupTypeId` on the regular group update (`HTTP 400 groupTypeId: validation.always.invalid`) and requires `POST /groups/{id}/grouptype` with a role mapping. ct still plans it as a normal update, so the apply fails (#171). Until that is fixed, change a group's type in the ChurchTools UI and adopt the drift. | | `groupStatusId` | **managed** | Lifecycle status; already managed. | | **`campusId`** | **managed (new, #21)** | The campus link is the tool's core "instantiate this area per campus" requirement. Numeric escape hatch only — an existing CT campus id (or `null` to clear). A _logical_ `campus: "key"` reference (resolving a same-run campus by key) is **deferred to [#20](https://github.com/eqrm/ct-cli/issues/20)**; the DSL rejects a `campus` field with a pointer to #20 so it can't slip through as an un-diffable phantom. | | `parents` (hierarchy) | **opt-in synthetic** | Group→group hierarchy is reconciled through its own endpoint, not the group body — see `src/engine/synthetic.ts`. Opt-in via `parents: [...]`. | diff --git a/docs/handbuch/dynamic-groups.md b/docs/handbuch/dynamic-groups.md index edff036..c1b57d7 100644 --- a/docs/handbuch/dynamic-groups.md +++ b/docs/handbuch/dynamic-groups.md @@ -305,7 +305,7 @@ At **capture** time (`ct adopt … --with-dynamic`) the state file and the ```text ! rulesets/jugend.json keeps 5 host-specific id(s) — NOT portable to another host: ctgroup.id: 1246 left numeric — not under management — `ct adopt group ` for each (then re-adopt) makes them portable - ctgroup.groupStatusId: 1, 2 left numeric — group statuses have no REST catalog (#67) — no logical form exists + ctgroup.groupStatusId: 1, 2 left numeric — no logical form yet; the catalog is `/person/masterdata` → `groupStatuses` (#157) person.id: 5703, 4389 left numeric — person ids are NEVER portable — ct does not manage people, so this ruleset names DIFFERENT people on another host. Remove the clause or accept the divergence ``` @@ -346,7 +346,7 @@ reason it never checked: The capture-time reasons are distinct because the fixes are: an **unmanaged** target (adopt it), a **role unknown to `/group/roles`**, a role whose **group type is unmanaged**, or a dimension with **no logical form at all** -(`ctgroup.groupStatusId` — group statuses have no REST catalog, #67; this one +(`ctgroup.groupStatusId` — no logical form yet, although `/person/masterdata` carries the catalog, #157; this one needs no lookup, so the plan-time scan reports it too). **`--strict-rulesets`** turns the warning into a refusal: adopt writes nothing diff --git a/docs/handbuch/field-definitions.md b/docs/handbuch/field-definitions.md index 1a1a3b1..b05d5c9 100644 --- a/docs/handbuch/field-definitions.md +++ b/docs/handbuch/field-definitions.md @@ -88,7 +88,14 @@ Evidence: `churchtools-api` tested @ CT 3.104, bensteUEM `ChurchToolsAPI` @ CT 3.101) and the ChurchTools Academy docs. **Re-verify** against a freshly generated schema per the re-audit procedure in `docs/runbook-manual-surface.md`. -2. **The REST field-definition resource is GET-only.** Every public client + +> **Update 2026-09-26:** the eqrm prod OpenAPI spec now lists REST writes on field definitions — +> `POST /dbfields`, `PUT`/`DELETE /dbfields/{fieldId}`, `POST /dbfields/{fieldId}/options`, +> `PUT`/`DELETE /dbfields/{fieldId}/options/{optionId}`. Points 2–3 below describe the state when +> this page was written. The endpoints are **not live-tested**, and the tool still treats data fields +> as read-only: promoting the resource is the follow-up this section describes at its end. + +2. **The REST field-definition resource was GET-only.** Every public client exposes data fields solely as `GET /dbfields` (list) and `GET /dbfields/{id}` (by id). No REST `POST`/`PUT`/`PATCH`/`DELETE` on a field-definition path exists in any of them. @@ -117,12 +124,12 @@ re-deciding rather than merely re-reading. ## Endpoint reference -| Purpose | Path | Methods (this CT) | `ct` surface | -| ------------------------------------------ | ------------------------------------------------------------------------------ | --------------------------------------- | --------------------------------------------------- | -| Person master-data model + security levels | `/person/masterdata` | GET (read-only) | `ct get person-masterdata` | -| Security levels, standalone | `/securitylevels`, `/securitylevels/{id}` | GET; POST/PATCH/DELETE on the item path | `ct get security-levels`, `ct.securityLevel` (#110) | -| Data-field definitions (person + group) | `/dbfields`, `/dbfields/{id}` | GET (read-only) | `ct get data-fields` | -| Field-definition **mutation** | legacy `churchdb` AJAX (`db_insertfields`/`db_updatefields`/`db_deletefields`) | non-REST | **not managed — manual** | +| Purpose | Path | Methods (this CT) | `ct` surface | +| ------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------ | --------------------------------------------------- | +| Person master-data model + security levels | `/person/masterdata` | GET (read-only) | `ct get person-masterdata` | +| Security levels, standalone | `/securitylevels`, `/securitylevels/{id}` | GET; POST/PATCH/DELETE on the item path | `ct get security-levels`, `ct.securityLevel` (#110) | +| Data-field definitions (person + group) | `/dbfields`, `/dbfields/{id}` | GET (writes in spec since 2026-09, unused) | `ct get data-fields` | +| Field-definition **mutation** | legacy `churchdb` AJAX (`db_insertfields`/`db_updatefields`/`db_deletefields`) | non-REST | **not managed — manual** | All paths verified against public CT client libraries + CT Academy docs, **not** against this repo's (git-ignored, ungenerated) `src/api/schema.d.ts`. diff --git a/docs/runbook-manual-surface.md b/docs/runbook-manual-surface.md index f1e28cf..0f63797 100644 --- a/docs/runbook-manual-surface.md +++ b/docs/runbook-manual-surface.md @@ -40,16 +40,16 @@ in that instance's own config repo, in a runbook following this doc's structure. ## Not yet implemented — API supports it, `ct` doesn't drive it yet -| Item | What it is | Tracking issue | Manual workaround today | -| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Group/group-type field decision table | Fields deliberately left unmanaged (decided out of scope): visibility, note, `autoAccept`/open-for-members, chat status, sort key. The triage **shipped** as a committed decision table ([`docs/group-field-decisions.md`](group-field-decisions.md)) | [#21](https://github.com/eqrm/ct-cli/issues/21) (decided) | Set by hand; these fields are intentionally not diffed — `ct` will neither preserve nor revert them. Promote one later only with its own registry entry + tests | -| Portable/logical references | **Shipped (#20, #25).** Configs reference master data by name/key — `campus`/`groupType` on a group, `ref.campus(...)` in ruleset `var` values, `groupType: ""` for a `group_type_role` domain, and now `group: "", role: ""` for a `group_role` domain (#25) — and the per-host resolver maps each to that instance's id at plan time (managed resources ∪ live catalogs). A same-run campus resolves at apply time. Numeric ids still work as an escape hatch. **`status` (group status) is NOT part of this** (#67) — group statuses have no REST catalog, so `status:` fails fast at eval time; declare the numeric `groupStatusId` directly | [#20](https://github.com/eqrm/ct-cli/issues/20) (done), [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | None needed for the shipped surface. Write logical names; run `ct plan`. The `group_role` pairing-id resolution is verified live (row below) | -| Environments (dev → prod promotion) | Named `(host, token, state file)` profiles and a `--env` flag; today one config + one state file = one host | [#22](https://github.com/eqrm/ct-cli/issues/22) | Point `CT_HOST`/state file manually at each target and re-run; keep dev and prod state files apart yourself, and be careful — nothing stops you from applying a dev-shaped config against prod today | -| Permission `group_role` domain by reference **(shipped, verified live)** | `ct.groupRole({ group, role })` now resolves the (group, role) pair to its pairing domainId at plan time (#25). **Confirmed live 2026-08-13 (CT 3.135.2):** it reads the group's role list (`GET /groups/{groupId}/roles`) and takes the matched role row's `id` as the pairing domainId. Two anchors on different group types: each row's `id` is a live `group_role` domainId carrying that role's grants, while its type-level `groupTypeRoleId` appears nowhere in the domainId set | [#25](https://github.com/eqrm/ct-cli/issues/25) (done, verified) | None needed. Works by reference for managed, already-created groups; numeric `id:` remains a supported escape hatch ([`docs/handbuch/permissions.md`](handbuch/permissions.md) "domainId semantics") | -| ~~Grant adoption~~ **(shipped)** | ~~existing rights structures must be hand-transcribed~~ — **`ct adopt grants ` ships this** (#25): it reads the live rows, applies the planner's normalization, and prints a paste-ready `ct.groupRole` / `ct.groupTypeRole` block (baseline/inherited excluded, denies noted-and-preserved, scope dataIds mapped back to managed-group keys). See [`docs/handbuch/permissions.md`](handbuch/permissions.md) "Adopting existing grants" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run `ct adopt grants group_role ` (or `group_type_role`), review the `WARNING`/`NOTE` comments, paste into config | -| ~~Permission catalog lifecycle~~ **(shipped)** | ~~`catalog.json` is a one-off HAR-trace snapshot with no staleness detection~~ — **shipped (#25):** `npm run regenerate:permission-catalog` rewrites it from a live instance (records the CT version in `$meta`), and `ct plan` now warns on a version mismatch or an unknown-authId live grant (which it leaves untouched, never revoking a right it cannot name). See [`docs/handbuch/permissions.md`](handbuch/permissions.md) "Catalog lifecycle & staleness" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run the command; heed the `ct plan` warnings | -| Field definitions & security levels (person + group custom fields) **(read-only, shipped #47/#48)** | The person master-data model, the security-level enumeration, and the data-field DEFINITIONS ("Datenfelder") for persons and groups — structural schema, not per-record values | [#47](https://github.com/eqrm/ct-cli/issues/47), [#48](https://github.com/eqrm/ct-cli/issues/48) (read shipped; write is an API gap — see note) | Read with `ct get person-masterdata` (model + security levels) and `ct get data-fields` (all field definitions, person + group, discriminated by `fieldCategory`). **Mutation stays manual:** field definitions have no REST write endpoint — only the legacy churchdb admin AJAX (`db_insertfields`/`db_updatefields`/`db_deletefields`) — so create/edit/delete them by hand in the master-data admin UI. Decision + evidence: [`docs/handbuch/field-definitions.md`](handbuch/field-definitions.md) | -| API re-audit for new CT releases | CT's OpenAPI spec is self-trimming (only shows endpoints your version has), so a new write endpoint (e.g. a member-status write, or — separately — a first-ever group-status list/write endpoint, #67) appears silently between CT upgrades | tracked by this issue ([#26](https://github.com/eqrm/ct-cli/issues/26)) | Procedure below (**Re-audit procedure for new CT releases**) | +| Item | What it is | Tracking issue | Manual workaround today | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Group/group-type field decision table | Fields deliberately left unmanaged (decided out of scope): visibility, note, `autoAccept`/open-for-members, chat status, sort key. The triage **shipped** as a committed decision table ([`docs/group-field-decisions.md`](group-field-decisions.md)) | [#21](https://github.com/eqrm/ct-cli/issues/21) (decided) | Set by hand; these fields are intentionally not diffed — `ct` will neither preserve nor revert them. Promote one later only with its own registry entry + tests | +| Portable/logical references | **Shipped (#20, #25).** Configs reference master data by name/key — `campus`/`groupType` on a group, `ref.campus(...)` in ruleset `var` values, `groupType: ""` for a `group_type_role` domain, and now `group: "", role: ""` for a `group_role` domain (#25) — and the per-host resolver maps each to that instance's id at plan time (managed resources ∪ live catalogs). A same-run campus resolves at apply time. Numeric ids still work as an escape hatch. **`status` (group status) is NOT part of this** (#67) — ct does not resolve group statuses by name yet, although `/person/masterdata` → `groupStatuses` is a read catalog (#157), so `status:` fails fast at eval time; declare the numeric `groupStatusId` directly | [#20](https://github.com/eqrm/ct-cli/issues/20) (done), [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | None needed for the shipped surface. Write logical names; run `ct plan`. The `group_role` pairing-id resolution is verified live (row below) | +| Environments (dev → prod promotion) | Named `(host, token, state file)` profiles and a `--env` flag; today one config + one state file = one host | [#22](https://github.com/eqrm/ct-cli/issues/22) | Point `CT_HOST`/state file manually at each target and re-run; keep dev and prod state files apart yourself, and be careful — nothing stops you from applying a dev-shaped config against prod today | +| Permission `group_role` domain by reference **(shipped, verified live)** | `ct.groupRole({ group, role })` now resolves the (group, role) pair to its pairing domainId at plan time (#25). **Confirmed live 2026-08-13 (CT 3.135.2):** it reads the group's role list (`GET /groups/{groupId}/roles`) and takes the matched role row's `id` as the pairing domainId. Two anchors on different group types: each row's `id` is a live `group_role` domainId carrying that role's grants, while its type-level `groupTypeRoleId` appears nowhere in the domainId set | [#25](https://github.com/eqrm/ct-cli/issues/25) (done, verified) | None needed. Works by reference for managed, already-created groups; numeric `id:` remains a supported escape hatch ([`docs/handbuch/permissions.md`](handbuch/permissions.md) "domainId semantics") | +| ~~Grant adoption~~ **(shipped)** | ~~existing rights structures must be hand-transcribed~~ — **`ct adopt grants ` ships this** (#25): it reads the live rows, applies the planner's normalization, and prints a paste-ready `ct.groupRole` / `ct.groupTypeRole` block (baseline/inherited excluded, denies noted-and-preserved, scope dataIds mapped back to managed-group keys). See [`docs/handbuch/permissions.md`](handbuch/permissions.md) "Adopting existing grants" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run `ct adopt grants group_role ` (or `group_type_role`), review the `WARNING`/`NOTE` comments, paste into config | +| ~~Permission catalog lifecycle~~ **(shipped)** | ~~`catalog.json` is a one-off HAR-trace snapshot with no staleness detection~~ — **shipped (#25):** `npm run regenerate:permission-catalog` rewrites it from a live instance (records the CT version in `$meta`), and `ct plan` now warns on a version mismatch or an unknown-authId live grant (which it leaves untouched, never revoking a right it cannot name). See [`docs/handbuch/permissions.md`](handbuch/permissions.md) "Catalog lifecycle & staleness" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run the command; heed the `ct plan` warnings | +| Field definitions & security levels (person + group custom fields) **(read-only, shipped #47/#48)** | The person master-data model, the security-level enumeration, and the data-field DEFINITIONS ("Datenfelder") for persons and groups — structural schema, not per-record values | [#47](https://github.com/eqrm/ct-cli/issues/47), [#48](https://github.com/eqrm/ct-cli/issues/48) (read shipped; write is an API gap — see note) | Read with `ct get person-masterdata` (model + security levels) and `ct get data-fields` (all field definitions, person + group, discriminated by `fieldCategory`). **Mutation stays manual:** field definitions have no REST write endpoint — only the legacy churchdb admin AJAX (`db_insertfields`/`db_updatefields`/`db_deletefields`) — so create/edit/delete them by hand in the master-data admin UI. Decision + evidence: [`docs/handbuch/field-definitions.md`](handbuch/field-definitions.md) | +| API re-audit for new CT releases | CT's OpenAPI spec is self-trimming (only shows endpoints your version has), so a new write endpoint (e.g. a member-status write, or — separately — a first-ever group-status list/write endpoint, #67) appears silently between CT upgrades | tracked by this issue ([#26](https://github.com/eqrm/ct-cli/issues/26)) | Procedure below (**Re-audit procedure for new CT releases**) | ## Out of tool scope — deliberate, not a gap From 16571d06129071c0a074e47cdd631d68922e7f1c Mon Sep 17 00:00:00 2001 From: Felix Kotschenreuther Date: Sat, 26 Sep 2026 14:09:05 +0200 Subject: [PATCH 2/2] docs(api-coverage): stop row 13 calling group statuses endpoint-less --- docs/api-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-coverage.md b/docs/api-coverage.md index 8246a8f..6862113 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -29,7 +29,7 @@ Methods marked only if they actually exist on the matched path. "Update" = PUT o | 10 | `ct_target_group` | `/group/targetgroups`, `/group/targetgroups/{targetGroupId}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | | 11 | `ct_meeting_point` | — none — | ❌ | ❌ | ❌ | ❌ | **Not in API → fully manual.** Zero matches for `treffpunkt`/`meetingpoint`/`meeting point` anywhere in the spec. Closest neighbours are _meeting templates_ (`/group/meetingtemplates`, full CRUD) and _group meetings_ (`/groups/{groupId}/meetings`, CRUD) — different concepts; confirm with product whether "meeting point" was meant to be one of those. | | 12 | `ct_relationship_type` | `/person/relationshiptypes`, `/person/relationshiptypes/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD** | -| 13 | `ct_person_status` | `/statuses`, `/statuses/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD, live-verified 2026-08-13** (eqrm prod, CT 3.135.2, read from the instance OpenAPI spec): `/statuses` → GET, POST; `/statuses/{id}` → GET, PUT, DELETE. `POST` requires `name`, `shorty`, `isMember`; `PUT` requires ALL of `name`, `shorty`, `isMember`, `isSearchable`, `sortKey`, `securityLevelId` — uniquely strict among managed types (every other managed PUT declares no required fields), and since PUT is a full replace the registry manages all six rather than a subset. The person/community **Status** master data (tag `Status`): "0 - First", "3 - Group Active", …, the domain a `ct.status` permission grant hangs off. Adoptable since #96, which is what makes a config using that domain self-sufficient across hosts. Do NOT conflate with `ct_group_status` (row 8, no endpoint at all) or `/group/memberstatus` (member statuses, string ids). Master data — never a person record; the people guard is unaffected. | +| 13 | `ct_person_status` | `/statuses`, `/statuses/{id}` | list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | **Full CRUD, live-verified 2026-08-13** (eqrm prod, CT 3.135.2, read from the instance OpenAPI spec): `/statuses` → GET, POST; `/statuses/{id}` → GET, PUT, DELETE. `POST` requires `name`, `shorty`, `isMember`; `PUT` requires ALL of `name`, `shorty`, `isMember`, `isSearchable`, `sortKey`, `securityLevelId` — uniquely strict among managed types (every other managed PUT declares no required fields), and since PUT is a full replace the registry manages all six rather than a subset. The person/community **Status** master data (tag `Status`): "0 - First", "3 - Group Active", …, the domain a `ct.status` permission grant hangs off. Adoptable since #96, which is what makes a config using that domain self-sufficient across hosts. Do NOT conflate with `ct_group_status` (row 8, a read-only catalog in `/person/masterdata`) or `/group/memberstatus` (member statuses, string ids). Master data — never a person record; the people guard is unaffected. | | 14 | `ct_department` | `/departments` | list ✅ / by-id ❌ | ❌ | ❌ | ❌ | **No REST write path — a ref catalog here, not a managed resource.** Live-probed 2026-08-13 (eqrm prod, CT 3.135.2): `GET /departments` returns `[{id, name, nameTranslated, sortKey, shorty}]`; no `POST`/`PUT`/`DELETE` on `/departments` exists in the spec, and there is no `/departments/{id}` path at all. Bereiche are the `cdb_bereich` permission scope dimension (`churchdb:view alldata`), so `ct` resolves them BY NAME for a `scope: [{ department: "…" }]` reference (#98) and surfaces them via `ct get departments` — but `ct.department` declares one and `ct adopt department ` adopts one. **Managed since #108** — `ct` writes Bereiche through the legacy `POST /index.php?q=churchdb/ajax` `func=saveMasterData` interface the admin UI uses (create/update/delete all verified live on eqrm-dev 2026-08-14). It appears in no OpenAPI spec, which is why an OpenAPI-only audit could never have found it (#111). Note there is no `/departments/{id}` at all, so a single Bereich is read by filtering the collection. | | 15 | `ct_group_member_field` | `/groups/{groupId}/memberfields` (GET), `/groups/{groupId}/memberfields/group` (POST), `/groups/{groupId}/memberfields/group/{groupMemberFieldId}` (PATCH/PUT/DELETE) | list ✅ (per group) / by-id ❌ | ✅ | ✅ PATCH (PUT fallback) | ✅ | **Full CRUD, but GROUP-SCOPED — not a standalone resource (#135).** A member field belongs to exactly one group and is not globally reusable, so it has no collection path of its own and no entry in the resource registry: it is a _synthetic owned sub-resource_ of `ct_group`, declared inside `ct.group({ memberFields: [...] })`, exactly like hierarchy `parents` and the dynamic ruleset. Its portable identity is the managed group key plus a local field key (`ojbp_2026_27_praktikum_1::wahl`); a ChurchTools field id never reaches authored config or an adopted blueprint. Reads are narrowed to rows the `/memberfields/group` endpoints own — the same GET also returns fields sourced from person master data / group-type defaults, which are neither adoptable nor writable here. `apply` NEVER deletes one (a field dropped from config produces no desired diff key at all); removal is the explicit `ct destroy --member-field ::`. Update uses `PATCH` (partial, so unmanaged siblings survive) and falls back to `PUT` on a 405/501. Definitions only — never a per-person value; `assertNotPeople` still guards every path. Distinct from the group CUSTOM fields of #48/#60 (`/dbfields`, `fieldCategory.table == "cdb_gruppe"`), which describe the group RECORD rather than what its members are asked. |