From 9e1754bbb06632370257d0eb2dc156160e82daa6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 7 May 2026 22:28:10 +0200 Subject: [PATCH] docs: add Reader glossary entry + clarifying comments on specs and query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GLOSSARY.md: defines "Reader" as the permission-derived role (isReader = !canWrite && !canAnnotate) — addresses @Markus blocker - GeschichteSpecifications.hasAuthor: comment explains null = no restriction (PUBLISHED path) — addresses @Markus suggestion - PersonRepository.findTopByDocumentCount: comment explains alias-in-ORDER-BY is intentional PostgreSQL behaviour — addresses @Markus suggestion Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/geschichte/GeschichteSpecifications.java | 1 + .../org/raddatz/familienarchiv/person/PersonRepository.java | 2 ++ docs/GLOSSARY.md | 3 +++ 3 files changed, 6 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSpecifications.java b/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSpecifications.java index 6834a783..42797ffe 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSpecifications.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSpecifications.java @@ -42,6 +42,7 @@ public final class GeschichteSpecifications { }; } + // null authorId → no restriction (PUBLISHED path passes null; Spring Data skips null predicates) public static Specification hasAuthor(UUID authorId) { return (root, query, cb) -> authorId == null ? null : cb.equal(root.get("author").get("id"), authorId); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java index a0e3fb16..6f431b74 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java @@ -69,6 +69,8 @@ public interface PersonRepository extends JpaRepository { nativeQuery = true) List searchWithDocumentCount(@Param("query") String query); + // ORDER BY uses the computed alias "documentCount" — valid PostgreSQL (aliases allowed in ORDER BY, + // unlike WHERE/HAVING). This is intentional; it would silently fail on MySQL or H2. @Query(value = """ SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName, p.person_type AS personType, diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 2b7ebe42..f1c75053 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -13,6 +13,9 @@ For domain package structure see [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) _(com **AppUser** (`AppUser`) — a real person who can log into the system (a family member or administrator). `AppUser` records carry login credentials, group memberships, and notification history. _Not to be confused with [Person](#person-person)_ — an AppUser is never recorded as a document sender, receiver, or historical individual. +**Reader** — an `AppUser` whose effective permissions include `READ_ALL` but neither `WRITE_ALL` nor `ANNOTATE_ALL`. Readers see a dedicated dashboard (`isReader = !canWrite && !canAnnotate`) focused on browsing documents, persons, and stories rather than contribution tasks. A user who also holds `BLOG_WRITE` is still classified as a Reader and additionally sees a drafts module. +_Not to be confused with [AppUser](#appuser-appuser)_ — Reader is a permission-derived role, not an entity. + **Permission** — a discrete capability string assigned to a `UserGroup` (e.g. `READ_ALL`, `WRITE_ALL`, `ADMIN`, `ADMIN_USER`, `ADMIN_TAG`, `ADMIN_PERMISSION`). Enforced via the `@RequirePermission` AOP annotation on controller methods, checked at runtime by `PermissionAspect`; not via Spring Security's `@PreAuthorize`. **Person** (`Person`) — a historical individual in the family archive (sender, receiver of letters, person mentioned in transcriptions). NEVER has a login account and NEVER appears as an `AppUser`.