Skip to content

docs(plans): plan 0017 — GAR-393 slice 1 (PATCH /v1/groups/{id})#22

Merged
michelbr84 merged 1 commit intomainfrom
docs/plan-0017-draft
Apr 16, 2026
Merged

docs(plans): plan 0017 — GAR-393 slice 1 (PATCH /v1/groups/{id})#22
michelbr84 merged 1 commit intomainfrom
docs/plan-0017-draft

Conversation

@michelbr84
Copy link
Copy Markdown
Owner

Summary

Docs-only PR adding plan 0017 as draft and registering it in the index. No code touched.

Plan at a glance

Linear: GAR-393 — "Rotas POST/GET/PATCH /v1/groups com OpenAPI" (Backlog, High, epic:ws-api).

Slice: 1 of 3 of GAR-393. POST + GET already shipped via plan 0016 M4. This plan delivers only PATCH /v1/groups/{group_id}.

Out of scope (explicit by operational decision 2026-04-15):

  • LIST /v1/groupsnot in GAR-393's formal endpoint list; gets a sub-issue or standalone ticket later.
  • invites / members / setRole / DELETE member — slice 2, plan 0018+.
  • Soft-delete, ETag, schemathesis, concurrency control.
  • Items from plan 0016 index comment tail (admin_url accessor, exec_with_tenant closure, etc).

Gate 2 — pre-plan empirical validation (2026-04-15)

Before writing the plan, verified each risk against the actual repo:

Item Outcome Source
groups table under RLS? No (app-layer authz only) migrations/001_initial_users_groups.sql:100-101, 007_row_level_security.sql:19-21
garraia_app has UPDATE grant on groups? ✅ Yes (schema-wide) migrations/007_row_level_security.sql:70
Action::GroupSettings exists? ✅ Yes crates/garraia-auth/src/action.rs:37
Owner + Admin have GroupSettings? ✅ Yes (Member/Guest/Child don't) crates/garraia-auth/src/can.rs:6-7
updated_at has trigger? No — handler must set explicitly migrations/001_initial_users_groups.sql:115
type = 'personal' API-visible? Reserved programmatic-only migrations/001_initial_users_groups.sql:114

Conclusion: zero new migration, zero new role, zero enum change, zero ADR. Pure reuse of plan 0016's foundation (AppPool + harness + authz_http_matrix).

Design invariants (non-negotiable for this slice)

Documented in the plan header:

  1. HTTP verb = PATCH, semantics = partial modification. Handler accepts any subset of mutable fields (name, type) and only updates what's explicitly present via COALESCE($new, column). Not a PUT (full-resource replacement). Empty body {} → 400, not no-op 200.
  2. updated_at set explicitly by the handler on every UPDATE. Schema has no trigger — forgetting this is a silent staleness bug. The UPDATE query always contains updated_at = now() in SET, regardless of which columns the caller changed.
  3. type = "personal" rejected with HTTP 400, defense-in-depth at 3 layers: (a) UpdateGroupRequest::validate, (b) unit test in groups.rs, (c) integration test in rest_v1_groups.rs. Any single-layer regression is caught before merge.
  4. Empty body → 400 deterministic detail "patch body must set at least one field".

Shape

  • 7 tasks + 1 validation pass (Task 0..6 + Task 7 validation-only)
  • ~6 commits, ~330 LOC (150 production + 180 test)
  • Files touched: rest_v1/groups.rs, rest_v1/mod.rs, rest_v1/openapi.rs, tests/rest_v1_groups.rs, tests/authz_http_matrix.rs, (conditional) garraia-auth/src/role.rs
  • authz_http_matrix expansion: MANDATORY — from 15 to 18/19 cases (PATCH × owner/admin/member/outsider)
  • security-auditor: reserved for the code PR review, not this docs PR — first mutation endpoint on tenant-root resource warrants it

Open questions — all closed before approval

  1. PATCH response body → 200 + full GroupReadResponse (reuses GET struct).
  2. Empty body → 400, not 200 no-op.
  3. Type changes → allowed for family/team, rejected for personal.
  4. Owner transfer via PATCH → No — belongs to MembersManage/GroupDelete.

Test plan

  • Markdown renders (plan file + index row).
  • Plan sections consistent (self-review checklist passed).
  • No code paths touched.

Execution plan (next step, after this docs PR merges)

  • (B) Inline execution in a dedicated feat/0017-gar-393-v1-groups-patch branch, batch with checkpoints (commit per task, stop and report at each milestone).
  • @security-auditor invoked on the code PR before merge.

🤖 Generated with Claude Code

Slice 1 of 3 of GAR-393 ("Rotas POST/GET/PATCH /v1/groups com OpenAPI").
POST and GET already shipped via plan 0016 M4. This plan covers only
PATCH /v1/groups/{group_id}; invites/members/setRole/DELETE member stay
out of scope for plan 0018+ (slice 2).

LIST /v1/groups is explicitly NOT in GAR-393's formal endpoint list and
stays out of this slice entirely — either a sub-issue or a standalone
GAR-XXX will carry it if/when it becomes necessary.

Pre-plan validation (gate 2, 2026-04-15) confirmed empirically:
- groups table has no RLS (app-layer authz only; migrations 001 + 007)
- garraia_app has GRANT UPDATE ON ALL TABLES (migration 007:70)
- Action::GroupSettings exists and Owner/Admin carry it (action.rs + can.rs)
- updated_at has no trigger — handler must set it explicitly (001:115)
- type='personal' reserved, handler must reject with 400 (001:114)

Zero migration, zero new role, zero enum change, zero ADR. Reuses 100%
of plan 0016's AppPool + harness + authz_http_matrix foundation.

Design invariants documented:
1. PATCH semantics = partial modification via COALESCE (not PUT)
2. updated_at = now() set explicitly in every UPDATE
3. type='personal' rejected with 400 at 3 layers (validate fn, unit test, integration test)
4. empty body rejected with 400 deterministic detail

7 tasks + 1 validation pass. ~330 LOC estimated (150 production + 180
test). authz_http_matrix expansion from 15 to 18/19 cases (PATCH ×
owner/admin/member/outsider) is mandatory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@michelbr84 michelbr84 merged commit a602c2f into main Apr 16, 2026
9 checks passed
@michelbr84 michelbr84 deleted the docs/plan-0017-draft branch April 16, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant