feat(geschichte): bound title and JOURNEY intro at all three layers

Title: requireTitle() turns the VARCHAR(255) DB bound into a friendly 400
(GESCHICHTE_TITLE_TOO_LONG). JOURNEY intro: MAX_INTRO_LENGTH = 4000 in
bodyForType() plus a V75 CHECK as atomic backstop (defensive clamp first,
STORY bodies exempt) — GESCHICHTE_INTRO_TOO_LONG. Both codes wired through
ErrorCode.java, errors.ts, getErrorMessage and de/en/es. DB-layer boundary
pins added: exactly-2000 note insert (V74 CHECK) and 4000/4001 intro
inserts against real Postgres. Docs: error-code lists, db puml diagrams.

The matching maxlength attributes land with the component commits.

Review round 3: Markus (b), Nora (1), Sara (DB 2000 boundary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-11 08:28:08 +02:00
parent 98805f596a
commit b9bed19610
14 changed files with 244 additions and 5 deletions

View File

@@ -1213,6 +1213,8 @@
"journey_publish_disabled_title": "Titel und mindestens ein Eintrag erforderlich",
"journey_save_hint_published": "Änderungen werden sofort für alle Leser sichtbar.",
"error_journey_note_too_long": "Die Notiz ist zu lang (maximal 2000 Zeichen).",
"error_geschichte_title_too_long": "Der Titel ist zu lang (maximal 255 Zeichen).",
"error_geschichte_intro_too_long": "Die Einleitung ist zu lang (maximal 4000 Zeichen).",
"error_journey_document_already_added": "Dieser Brief ist bereits in der Lesereise enthalten.",
"error_geschichte_type_immutable": "Der Typ einer Geschichte kann nach der Erstellung nicht mehr geändert werden."
}

View File

@@ -1213,6 +1213,8 @@
"journey_publish_disabled_title": "Title and at least one entry required",
"journey_save_hint_published": "Changes will be immediately visible to all readers.",
"error_journey_note_too_long": "The note is too long (maximum 2000 characters).",
"error_geschichte_title_too_long": "The title is too long (maximum 255 characters).",
"error_geschichte_intro_too_long": "The introduction is too long (maximum 4000 characters).",
"error_journey_document_already_added": "This letter is already included in the reading journey.",
"error_geschichte_type_immutable": "The type of a story cannot be changed after creation."
}

View File

@@ -1213,6 +1213,8 @@
"journey_publish_disabled_title": "Se requiere título y al menos una entrada",
"journey_save_hint_published": "Los cambios serán visibles inmediatamente para todos los lectores.",
"error_journey_note_too_long": "La nota es demasiado larga (máximo 2000 caracteres).",
"error_geschichte_title_too_long": "El título es demasiado largo (máximo 255 caracteres).",
"error_geschichte_intro_too_long": "La introducción es demasiado larga (máximo 4000 caracteres).",
"error_journey_document_already_added": "Esta carta ya está incluida en el viaje de lectura.",
"error_geschichte_type_immutable": "El tipo de una historia no se puede cambiar después de su creación."
}

View File

@@ -53,6 +53,8 @@ export type ErrorCode =
| 'JOURNEY_DOCUMENT_ALREADY_ADDED'
| 'GESCHICHTE_TYPE_MISMATCH'
| 'GESCHICHTE_TYPE_IMMUTABLE'
| 'GESCHICHTE_TITLE_TOO_LONG'
| 'GESCHICHTE_INTRO_TOO_LONG'
| 'INVALID_CREDENTIALS'
| 'SESSION_EXPIRED'
| 'MISSING_CREDENTIALS'
@@ -185,6 +187,10 @@ export function getErrorMessage(code: ErrorCode | string | undefined): string {
return m.error_geschichte_type_mismatch();
case 'GESCHICHTE_TYPE_IMMUTABLE':
return m.error_geschichte_type_immutable();
case 'GESCHICHTE_TITLE_TOO_LONG':
return m.error_geschichte_title_too_long();
case 'GESCHICHTE_INTRO_TOO_LONG':
return m.error_geschichte_intro_too_long();
case 'INVALID_CREDENTIALS':
return m.error_invalid_credentials();
case 'SESSION_EXPIRED':