Delete the JOURNEY-only type guard in JourneyItemService.append() so the existing item endpoints serve both Geschichte types. GeschichteType has exactly two constants, so an allowlist replacement would be unreachable dead code. Fix the not-found messages that claimed "Journey", and remove the now-orphaned GESCHICHTE_TYPE_MISMATCH error code end to end (ErrorCode, errors.ts union + mapping, i18n keys in de/en/es). Tests: three STORY append unit tests written red against the guard, plus end-to-end STORY coverage (append+retrieve, V72-style position-gap rows incl. removal, dangling document-deleted item). The two STORY-rejection tests die with the guard — no third enum value exists to feed it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
215 lines
6.7 KiB
TypeScript
215 lines
6.7 KiB
TypeScript
import * as m from '$lib/paraglide/messages.js';
|
|
|
|
/**
|
|
* Mirror of the backend ErrorCode enum.
|
|
* Keep in sync with backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java
|
|
*/
|
|
export type ErrorCode =
|
|
| 'PERSON_NOT_FOUND'
|
|
| 'ALIAS_NOT_FOUND'
|
|
| 'INVALID_PERSON_TYPE'
|
|
| 'INVALID_DATE_RANGE'
|
|
| 'DOCUMENT_NOT_FOUND'
|
|
| 'DOCUMENT_NO_FILE'
|
|
| 'FILE_NOT_FOUND'
|
|
| 'FILE_UPLOAD_FAILED'
|
|
| 'UNSUPPORTED_FILE_TYPE'
|
|
| 'USER_NOT_FOUND'
|
|
| 'EMAIL_ALREADY_IN_USE'
|
|
| 'WRONG_CURRENT_PASSWORD'
|
|
| 'IMPORT_ALREADY_RUNNING'
|
|
| 'IMPORT_ARTIFACT_INVALID'
|
|
| 'INVALID_RESET_TOKEN'
|
|
| 'INVITE_NOT_FOUND'
|
|
| 'INVITE_EXHAUSTED'
|
|
| 'INVITE_REVOKED'
|
|
| 'INVITE_EXPIRED'
|
|
| 'GROUP_HAS_ACTIVE_INVITES'
|
|
| 'GROUP_NOT_FOUND'
|
|
| 'ANNOTATION_NOT_FOUND'
|
|
| 'ANNOTATION_UPDATE_FAILED'
|
|
| 'TRANSCRIPTION_BLOCK_NOT_FOUND'
|
|
| 'TRANSCRIPTION_BLOCK_CONFLICT'
|
|
| 'COMMENT_NOT_FOUND'
|
|
| 'OCR_SERVICE_UNAVAILABLE'
|
|
| 'OCR_JOB_NOT_FOUND'
|
|
| 'OCR_DOCUMENT_NOT_UPLOADED'
|
|
| 'OCR_PROCESSING_FAILED'
|
|
| 'TRAINING_ALREADY_RUNNING'
|
|
| 'OCR_TRAINING_CONFLICT'
|
|
| 'INVALID_TAG_COLOR'
|
|
| 'TAG_CYCLE_DETECTED'
|
|
| 'TAG_NOT_FOUND'
|
|
| 'TAG_MERGE_SELF'
|
|
| 'TAG_MERGE_INVALID_TARGET'
|
|
| 'RELATIONSHIP_NOT_FOUND'
|
|
| 'CIRCULAR_RELATIONSHIP'
|
|
| 'DUPLICATE_RELATIONSHIP'
|
|
| 'GESCHICHTE_NOT_FOUND'
|
|
| 'JOURNEY_ITEM_NOT_FOUND'
|
|
| 'JOURNEY_ITEM_POSITION_CONFLICT'
|
|
| 'JOURNEY_AT_CAPACITY'
|
|
| 'JOURNEY_NOTE_TOO_LONG'
|
|
| 'JOURNEY_DOCUMENT_ALREADY_ADDED'
|
|
| 'GESCHICHTE_TYPE_IMMUTABLE'
|
|
| 'GESCHICHTE_TITLE_TOO_LONG'
|
|
| 'GESCHICHTE_INTRO_TOO_LONG'
|
|
| 'INVALID_CREDENTIALS'
|
|
| 'SESSION_EXPIRED'
|
|
| 'MISSING_CREDENTIALS'
|
|
| 'UNAUTHORIZED'
|
|
| 'FORBIDDEN'
|
|
| 'CSRF_TOKEN_MISSING'
|
|
| 'TOO_MANY_LOGIN_ATTEMPTS'
|
|
| 'VALIDATION_ERROR'
|
|
| 'BATCH_TOO_LARGE'
|
|
| 'BULK_EDIT_TOO_MANY_IDS'
|
|
| 'INTERNAL_ERROR';
|
|
|
|
export interface BackendError {
|
|
code: ErrorCode;
|
|
message: string; // English developer message — not shown to users
|
|
}
|
|
|
|
/**
|
|
* Attempts to parse a backend ErrorResponse from a failed fetch response.
|
|
* Returns null if the body is not valid JSON or does not contain a code field.
|
|
*/
|
|
export async function parseBackendError(res: Response): Promise<BackendError | null> {
|
|
try {
|
|
const body = await res.json();
|
|
if (body && typeof body.code === 'string') {
|
|
return body as BackendError;
|
|
}
|
|
} catch {
|
|
// Body was not JSON
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Returns a localised message for the given error code via Paraglide. Falls back to INTERNAL_ERROR. */
|
|
export function getErrorMessage(code: ErrorCode | string | undefined): string {
|
|
switch (code) {
|
|
case 'PERSON_NOT_FOUND':
|
|
return m.error_person_not_found();
|
|
case 'ALIAS_NOT_FOUND':
|
|
return m.error_alias_not_found();
|
|
case 'INVALID_PERSON_TYPE':
|
|
return m.error_invalid_person_type();
|
|
case 'INVALID_DATE_RANGE':
|
|
return m.error_invalid_date_range();
|
|
case 'DOCUMENT_NOT_FOUND':
|
|
return m.error_document_not_found();
|
|
case 'DOCUMENT_NO_FILE':
|
|
return m.error_document_no_file();
|
|
case 'FILE_NOT_FOUND':
|
|
return m.error_file_not_found();
|
|
case 'FILE_UPLOAD_FAILED':
|
|
return m.error_file_upload_failed();
|
|
case 'UNSUPPORTED_FILE_TYPE':
|
|
return m.error_unsupported_file_type();
|
|
case 'USER_NOT_FOUND':
|
|
return m.error_user_not_found();
|
|
case 'EMAIL_ALREADY_IN_USE':
|
|
return m.error_email_already_in_use();
|
|
case 'WRONG_CURRENT_PASSWORD':
|
|
return m.error_wrong_current_password();
|
|
case 'IMPORT_ALREADY_RUNNING':
|
|
return m.error_import_already_running();
|
|
case 'IMPORT_ARTIFACT_INVALID':
|
|
return m.error_import_artifact_invalid();
|
|
case 'INVALID_RESET_TOKEN':
|
|
return m.error_invalid_reset_token();
|
|
case 'INVITE_NOT_FOUND':
|
|
return m.error_invite_not_found();
|
|
case 'INVITE_EXHAUSTED':
|
|
return m.error_invite_exhausted();
|
|
case 'INVITE_REVOKED':
|
|
return m.error_invite_revoked();
|
|
case 'INVITE_EXPIRED':
|
|
return m.error_invite_expired();
|
|
case 'GROUP_HAS_ACTIVE_INVITES':
|
|
return m.error_group_has_active_invites();
|
|
case 'GROUP_NOT_FOUND':
|
|
return m.error_group_not_found();
|
|
case 'ANNOTATION_NOT_FOUND':
|
|
return m.error_annotation_not_found();
|
|
case 'ANNOTATION_UPDATE_FAILED':
|
|
return m.error_annotation_update_failed();
|
|
case 'TRANSCRIPTION_BLOCK_NOT_FOUND':
|
|
return m.error_transcription_block_not_found();
|
|
case 'TRANSCRIPTION_BLOCK_CONFLICT':
|
|
return m.error_transcription_block_conflict();
|
|
case 'COMMENT_NOT_FOUND':
|
|
return m.error_comment_not_found();
|
|
case 'OCR_SERVICE_UNAVAILABLE':
|
|
return m.error_ocr_service_unavailable();
|
|
case 'OCR_JOB_NOT_FOUND':
|
|
return m.error_ocr_job_not_found();
|
|
case 'OCR_DOCUMENT_NOT_UPLOADED':
|
|
return m.error_ocr_document_not_uploaded();
|
|
case 'OCR_PROCESSING_FAILED':
|
|
return m.error_ocr_processing_failed();
|
|
case 'TRAINING_ALREADY_RUNNING':
|
|
return m.error_training_already_running();
|
|
case 'OCR_TRAINING_CONFLICT':
|
|
return m.error_internal_error();
|
|
case 'INVALID_TAG_COLOR':
|
|
return m.error_invalid_tag_color();
|
|
case 'TAG_CYCLE_DETECTED':
|
|
return m.error_tag_cycle_detected();
|
|
case 'TAG_NOT_FOUND':
|
|
return m.error_tag_not_found();
|
|
case 'TAG_MERGE_SELF':
|
|
return m.error_tag_merge_self();
|
|
case 'TAG_MERGE_INVALID_TARGET':
|
|
return m.error_tag_merge_invalid_target();
|
|
case 'RELATIONSHIP_NOT_FOUND':
|
|
return m.error_relationship_not_found();
|
|
case 'CIRCULAR_RELATIONSHIP':
|
|
return m.error_circular_relationship();
|
|
case 'DUPLICATE_RELATIONSHIP':
|
|
return m.error_duplicate_relationship();
|
|
case 'GESCHICHTE_NOT_FOUND':
|
|
return m.error_geschichte_not_found();
|
|
case 'JOURNEY_ITEM_NOT_FOUND':
|
|
return m.error_journey_item_not_found();
|
|
case 'JOURNEY_ITEM_POSITION_CONFLICT':
|
|
return m.error_journey_item_position_conflict();
|
|
case 'JOURNEY_AT_CAPACITY':
|
|
return m.error_journey_at_capacity();
|
|
case 'JOURNEY_NOTE_TOO_LONG':
|
|
return m.error_journey_note_too_long();
|
|
case 'JOURNEY_DOCUMENT_ALREADY_ADDED':
|
|
return m.error_journey_document_already_added();
|
|
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':
|
|
return m.error_session_expired();
|
|
case 'MISSING_CREDENTIALS':
|
|
return m.login_error_missing_credentials();
|
|
case 'UNAUTHORIZED':
|
|
return m.error_unauthorized();
|
|
case 'FORBIDDEN':
|
|
return m.error_forbidden();
|
|
case 'CSRF_TOKEN_MISSING':
|
|
return m.error_csrf_token_missing();
|
|
case 'TOO_MANY_LOGIN_ATTEMPTS':
|
|
return m.error_too_many_login_attempts();
|
|
case 'VALIDATION_ERROR':
|
|
return m.error_validation_error();
|
|
case 'BATCH_TOO_LARGE':
|
|
return m.error_batch_too_large();
|
|
case 'BULK_EDIT_TOO_MANY_IDS':
|
|
return m.error_bulk_edit_too_many_ids();
|
|
default:
|
|
return m.error_internal_error();
|
|
}
|
|
}
|