docs(timeline): pin relative issue ordinals to Gitea issue numbers
The issue body's milestone-relative ordinals ("issue 3", "issue 5") become
unreadable once the milestone closes. Resolved against the Zeitstrahl milestone:
issue 3 = #775 (CRUD API: service/controller/DTO), issue 5 = #777 (assembly
endpoint with the per-person filter). Mapping anchored by issue 6 = #778
(date-label helper) and issue 9 = #781 (curator forms) in #774's forward notes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -122,7 +122,7 @@ public class TimelineEvent {
|
|||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optimistic-lock version for the multi-curator edit flow (issue 3). Object {@code Long}
|
* Optimistic-lock version for the multi-curator edit flow (#775). Object {@code Long}
|
||||||
* (not primitive) so it is {@code null} before first persist; Hibernate sets {@code 0} on
|
* (not primitive) so it is {@code null} before first persist; Hibernate sets {@code 0} on
|
||||||
* insert. A concurrent-write conflict must be translated to {@code DomainException.conflict}
|
* insert. A concurrent-write conflict must be translated to {@code DomainException.conflict}
|
||||||
* in the service layer (ADR-040) — otherwise it surfaces as HTTP 500 with Hibernate
|
* in the service layer (ADR-040) — otherwise it surfaces as HTTP 500 with Hibernate
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface TimelineEventRepository extends JpaRepository<TimelineEvent, UUID> {
|
public interface TimelineEventRepository extends JpaRepository<TimelineEvent, UUID> {
|
||||||
// TODO(issue 5): findByPersonsContaining(Person) needed for the per-person filter
|
// TODO(#777): findByPersonsContaining(Person) needed for the per-person filter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ audit footprint deliberately diverges (see below).
|
|||||||
Curated timeline events are their own concern, not a Lesereise/`Geschichte` subtype.
|
Curated timeline events are their own concern, not a Lesereise/`Geschichte` subtype.
|
||||||
The domain owns `TimelineEvent`, `EventType`, and `TimelineEventRepository`.
|
The domain owns `TimelineEvent`, `EventType`, and `TimelineEventRepository`.
|
||||||
|
|
||||||
### 2. Responses (issue 3) will be views, not serialized entities
|
### 2. Responses (#775) will be views, not serialized entities
|
||||||
|
|
||||||
`TimelineEvent` has two LAZY `ManyToMany` collections (`persons`, `documents`) and
|
`TimelineEvent` has two LAZY `ManyToMany` collections (`persons`, `documents`) and
|
||||||
`open-in-view` is `false` — exactly the shape that motivated ADR-036 for `geschichte`.
|
`open-in-view` is `false` — exactly the shape that motivated ADR-036 for `geschichte`.
|
||||||
Issue 3 must assemble `TimelineEventView`/`TimelineEventSummary` inside the service
|
#775 must assemble `TimelineEventView`/`TimelineEventSummary` inside the service
|
||||||
transaction; a serialized entity is a 500 waiting to happen. Decided up front so it is
|
transaction; a serialized entity is a 500 waiting to happen. Decided up front so it is
|
||||||
not retrofitted later.
|
not retrofitted later.
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ inferred. This divergence is deliberate: a future "bug fix" must not relax it to
|
|||||||
`Document` has neither a version nor a creator. A curated entity edited by multiple curators
|
`Document` has neither a version nor a creator. A curated entity edited by multiple curators
|
||||||
warrants real protection, so `TimelineEvent` adds:
|
warrants real protection, so `TimelineEvent` adds:
|
||||||
|
|
||||||
- `@Version Long version` — optimistic locking for the multi-curator edit flow (issue 3).
|
- `@Version Long version` — optimistic locking for the multi-curator edit flow (#775).
|
||||||
Object `Long` (not primitive) so it is `null` before first persist; Hibernate sets `0` on
|
Object `Long` (not primitive) so it is `null` before first persist; Hibernate sets `0` on
|
||||||
insert. The service **must** catch `ObjectOptimisticLockingFailureException` and translate
|
insert. The service **must** catch `ObjectOptimisticLockingFailureException` and translate
|
||||||
it to `DomainException.conflict(...)`. Without that translation a concurrent-write conflict
|
it to `DomainException.conflict(...)`. Without that translation a concurrent-write conflict
|
||||||
@@ -82,7 +82,7 @@ session principal before every `save()`, or the timestamp moves while the "who"
|
|||||||
### 7. `createdBy`/`updatedBy` are server-populated only — never bound from client input
|
### 7. `createdBy`/`updatedBy` are server-populated only — never bound from client input
|
||||||
|
|
||||||
Both are set from the session principal in the service, never from a request body. Binding
|
Both are set from the session principal in the service, never from a request body. Binding
|
||||||
them from client input is an authorship-forgery / mass-assignment vector (CWE-639). Issue 3's
|
them from client input is an authorship-forgery / mass-assignment vector (CWE-639). #775's
|
||||||
regression suite must include forgery cases on **both** write paths (`POST` body with
|
regression suite must include forgery cases on **both** write paths (`POST` body with
|
||||||
`createdBy`, `PUT` body with `updatedBy`) — create and update are separate binding paths, so
|
`createdBy`, `PUT` body with `updatedBy`) — create and update are separate binding paths, so
|
||||||
testing only one leaves half the vector open. The update test must assert `updatedBy` equals
|
testing only one leaves half the vector open. The update test must assert `updatedBy` equals
|
||||||
@@ -108,5 +108,5 @@ the event intact (V71/ADR-032 hardening — a person delete must never 500).
|
|||||||
`timeline_events`). No rollback script, no rollback test.
|
`timeline_events`). No rollback script, no rollback test.
|
||||||
- The `timeline → document.DatePrecision` compile coupling is permanent until a shared-package
|
- The `timeline → document.DatePrecision` compile coupling is permanent until a shared-package
|
||||||
refactor; precedent already exists (`importing/DocumentImporter`, `person`).
|
refactor; precedent already exists (`importing/DocumentImporter`, `person`).
|
||||||
- The service/controller/DTO layer (issue 3) inherits the view-assembly, optimistic-lock
|
- The service/controller/DTO layer (#775) inherits the view-assembly, optimistic-lock
|
||||||
translation, forgery-guard, and permission obligations recorded above.
|
translation, forgery-guard, and permission obligations recorded above.
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ title Component Diagram: API Backend — Timeline (Zeitstrahl)
|
|||||||
ContainerDb(db, "PostgreSQL", "PostgreSQL 16")
|
ContainerDb(db, "PostgreSQL", "PostgreSQL 16")
|
||||||
|
|
||||||
System_Boundary(backend, "API Backend (Spring Boot)") {
|
System_Boundary(backend, "API Backend (Spring Boot)") {
|
||||||
Component(timelineRepo, "TimelineEventRepository", "Spring Data JPA", "Reads and writes TimelineEvent rows and their persons/documents join tables (timeline_event_persons, timeline_event_documents). Issue #774 ships the repository empty; the per-person filter query lands in a later issue.")
|
Component(timelineRepo, "TimelineEventRepository", "Spring Data JPA", "Reads and writes TimelineEvent rows and their persons/documents join tables (timeline_event_persons, timeline_event_documents). Issue #774 ships the repository empty; the per-person filter query lands in #777.")
|
||||||
|
|
||||||
Component(timelineSvc, "TimelineEventService", "Spring Service (planned, issue 3)", "Will own curated-event CRUD: assemble TimelineEventView/Summary inside the transaction (lazy ManyToMany + open-in-view=false, per ADR-036/ADR-040), populate createdBy/updatedBy from the session principal, and translate optimistic-lock conflicts to DomainException.conflict.")
|
Component(timelineSvc, "TimelineEventService", "Spring Service (planned, #775)", "Will own curated-event CRUD: assemble TimelineEventView/Summary inside the transaction (lazy ManyToMany + open-in-view=false, per ADR-036/ADR-040), populate createdBy/updatedBy from the session principal, and translate optimistic-lock conflicts to DomainException.conflict.")
|
||||||
Component(timelineCtrl, "TimelineEventController", "Spring MVC (planned, issue 3)", "Will expose /api/timeline reads (READ_ALL) and writes (WRITE_ALL). createdBy/updatedBy are never bound from request bodies (CWE-639).")
|
Component(timelineCtrl, "TimelineEventController", "Spring MVC (planned, #775)", "Will expose /api/timeline reads (READ_ALL) and writes (WRITE_ALL). createdBy/updatedBy are never bound from request bodies (CWE-639).")
|
||||||
}
|
}
|
||||||
|
|
||||||
System_Ext(documentDomain, "Document domain", "Provides DatePrecision (shared value type) and Document references for linked letters")
|
System_Ext(documentDomain, "Document domain", "Provides DatePrecision (shared value type) and Document references for linked letters")
|
||||||
|
|||||||
Reference in New Issue
Block a user