mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1959: feat: enhance error handling and validation infrastructure
feat: enhance error handling and validation infrastructure - Add comprehensive Zod error helper with German localization - Migrate from deprecated .toPromise() to firstValueFrom() - Enhance global error handler with ZodError support - Implement storage features for signal stores with auto-save - Add comprehensive test coverage for validation scenarios - Update multiple stores with improved storage integration - Extend tab management with enhanced navigation patterns - Add checkout data-access barrel exports - Update core-storage documentation with usage examples Major improvements: - Complete German error message translations for all Zod validation types - Auto-save with configurable debouncing for signal stores - Type-safe storage integration with schema validation - Enhanced entity management with orphan cleanup - Robust fallback strategies for validation failures Breaking: Requires Zod validation errors to use new helper Refs: #5345 #5353 Related work items: #5345, #5353
This commit is contained in:
committed by
Nino Righi
parent
f2490b3421
commit
39a55c9d55
@@ -1 +1,2 @@
|
||||
export * from './create-esc-abort-controller.helper';
|
||||
export * from './zod-error.helper';
|
||||
|
||||
128
libs/common/data-access/src/lib/helpers/zod-error.helper.spec.ts
Normal file
128
libs/common/data-access/src/lib/helpers/zod-error.helper.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { ZodError, z } from 'zod';
|
||||
import { extractZodErrorMessage } from './zod-error.helper';
|
||||
|
||||
describe('ZodErrorHelper', () => {
|
||||
describe('extractZodErrorMessage', () => {
|
||||
it('should return default message for empty issues', () => {
|
||||
const error = new ZodError([]);
|
||||
const result = extractZodErrorMessage(error);
|
||||
|
||||
expect(result).toBe('Unbekannter Validierungsfehler aufgetreten.');
|
||||
});
|
||||
|
||||
it('should format single invalid_type error', () => {
|
||||
const schema = z.string();
|
||||
|
||||
try {
|
||||
schema.parse(123);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const result = extractZodErrorMessage(error);
|
||||
expect(result).toBe('Erwartet: Text, erhalten: Zahl');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should format invalid_string error for email validation', () => {
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
try {
|
||||
schema.parse({ email: 'invalid-email' });
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const result = extractZodErrorMessage(error);
|
||||
expect(result).toBe('email: Ungültige E-Mail-Adresse');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should format too_small error for strings', () => {
|
||||
const schema = z.object({
|
||||
name: z.string().min(5),
|
||||
});
|
||||
|
||||
try {
|
||||
schema.parse({ name: 'ab' });
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const result = extractZodErrorMessage(error);
|
||||
expect(result).toBe('name: Text muss mindestens 5 Zeichen lang sein');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should format multiple errors with bullet points', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
try {
|
||||
schema.parse({ name: 123, age: 'not-a-number' });
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const result = extractZodErrorMessage(error);
|
||||
|
||||
expect(result).toContain('Es sind 2 Validierungsfehler aufgetreten:');
|
||||
expect(result).toContain('• name: Erwartet: Text, erhalten: Zahl');
|
||||
expect(result).toContain('• age: Erwartet: Zahl, erhalten: Text');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should format nested path correctly', () => {
|
||||
const schema = z.object({
|
||||
user: z.object({
|
||||
profile: z.object({
|
||||
email: z.string().email(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
schema.parse({
|
||||
user: {
|
||||
profile: {
|
||||
email: 'invalid-email',
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const result = extractZodErrorMessage(error);
|
||||
expect(result).toBe('user → profile → email: Ungültige E-Mail-Adresse');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle array indices in paths', () => {
|
||||
const schema = z.array(z.string());
|
||||
|
||||
try {
|
||||
schema.parse(['valid', 123, 'also-valid']);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const result = extractZodErrorMessage(error);
|
||||
expect(result).toBe('[1]: Erwartet: Text, erhalten: Zahl');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle custom error messages', () => {
|
||||
const schema = z.string().refine((val) => val === 'specific', {
|
||||
message: 'Must be exactly "specific"',
|
||||
});
|
||||
|
||||
try {
|
||||
schema.parse('wrong');
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const result = extractZodErrorMessage(error);
|
||||
expect(result).toBe('Must be exactly "specific"');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
226
libs/common/data-access/src/lib/helpers/zod-error.helper.ts
Normal file
226
libs/common/data-access/src/lib/helpers/zod-error.helper.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { ZodError, ZodIssue } from 'zod';
|
||||
|
||||
/**
|
||||
* Extracts and formats human-readable error messages from ZodError instances.
|
||||
*
|
||||
* This function processes Zod validation errors and transforms them into
|
||||
* user-friendly messages that can be displayed in UI components.
|
||||
*
|
||||
* @param error - The ZodError instance to extract messages from
|
||||
* @returns A formatted string containing all validation error messages
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* schema.parse(data);
|
||||
* } catch (error) {
|
||||
* if (error instanceof ZodError) {
|
||||
* const message = extractZodErrorMessage(error);
|
||||
* // Display message to user
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function extractZodErrorMessage(error: ZodError): string {
|
||||
if (!error.issues || error.issues.length === 0) {
|
||||
return 'Unbekannter Validierungsfehler aufgetreten.';
|
||||
}
|
||||
|
||||
const messages = error.issues.map((issue) => formatZodIssue(issue));
|
||||
|
||||
// Remove duplicates and join with line breaks
|
||||
const uniqueMessages = Array.from(new Set(messages));
|
||||
|
||||
if (uniqueMessages.length === 1) {
|
||||
return uniqueMessages[0];
|
||||
}
|
||||
|
||||
return `Es sind ${uniqueMessages.length} Validierungsfehler aufgetreten:\n\n${uniqueMessages.map(msg => `• ${msg}`).join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a single ZodIssue into a human-readable message.
|
||||
*
|
||||
* @param issue - The ZodIssue to format
|
||||
* @returns A formatted error message string
|
||||
*/
|
||||
function formatZodIssue(issue: ZodIssue): string {
|
||||
const fieldPath = formatFieldPath(issue.path);
|
||||
const fieldPrefix = fieldPath ? `${fieldPath}: ` : '';
|
||||
|
||||
switch (issue.code) {
|
||||
case 'invalid_type':
|
||||
return `${fieldPrefix}${formatInvalidTypeMessage(issue)}`;
|
||||
|
||||
case 'too_small':
|
||||
return `${fieldPrefix}${formatTooSmallMessage(issue)}`;
|
||||
|
||||
case 'too_big':
|
||||
return `${fieldPrefix}${formatTooBigMessage(issue)}`;
|
||||
|
||||
case 'invalid_string':
|
||||
return `${fieldPrefix}${formatInvalidStringMessage(issue)}`;
|
||||
|
||||
case 'unrecognized_keys':
|
||||
return `${fieldPrefix}Unbekannte Felder: ${issue.keys?.join(', ')}`;
|
||||
|
||||
case 'invalid_union':
|
||||
return `${fieldPrefix}Wert entspricht nicht den erwarteten Optionen`;
|
||||
|
||||
case 'invalid_enum_value':
|
||||
return `${fieldPrefix}Ungültiger Wert. Erlaubt sind: ${issue.options?.join(', ')}`;
|
||||
|
||||
case 'invalid_arguments':
|
||||
return `${fieldPrefix}Ungültige Parameter`;
|
||||
|
||||
case 'invalid_return_type':
|
||||
return `${fieldPrefix}Ungültiger Rückgabetyp`;
|
||||
|
||||
case 'invalid_date':
|
||||
return `${fieldPrefix}Ungültiges Datum`;
|
||||
|
||||
case 'invalid_literal':
|
||||
return `${fieldPrefix}Wert muss exakt '${issue.expected}' sein`;
|
||||
|
||||
case 'custom':
|
||||
return `${fieldPrefix}${issue.message || 'Benutzerdefinierte Validierung fehlgeschlagen'}`;
|
||||
|
||||
default:
|
||||
return `${fieldPrefix}${issue.message || 'Validierungsfehler'}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a field path array into a human-readable string.
|
||||
*
|
||||
* @param path - Array of field path segments
|
||||
* @returns Formatted path string or empty string if no path
|
||||
*/
|
||||
function formatFieldPath(path: (string | number)[]): string {
|
||||
if (!path || path.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return path
|
||||
.map((segment) => {
|
||||
if (typeof segment === 'number') {
|
||||
return `[${segment}]`;
|
||||
}
|
||||
return segment;
|
||||
})
|
||||
.join(' → ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats invalid type error messages with German translations.
|
||||
*/
|
||||
function formatInvalidTypeMessage(issue: ZodIssue & { expected: string; received: string }): string {
|
||||
const typeTranslations: Record<string, string> = {
|
||||
string: 'Text',
|
||||
number: 'Zahl',
|
||||
boolean: 'Ja/Nein-Wert',
|
||||
object: 'Objekt',
|
||||
array: 'Liste',
|
||||
date: 'Datum',
|
||||
undefined: 'undefiniert',
|
||||
null: 'null',
|
||||
bigint: 'große Zahl',
|
||||
function: 'Funktion',
|
||||
symbol: 'Symbol',
|
||||
};
|
||||
|
||||
const expected = typeTranslations[issue.expected] || issue.expected;
|
||||
const received = typeTranslations[issue.received] || issue.received;
|
||||
|
||||
return `Erwartet: ${expected}, erhalten: ${received}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats "too small" error messages based on the type.
|
||||
*/
|
||||
function formatTooSmallMessage(issue: any): string {
|
||||
const { type, minimum, inclusive } = issue;
|
||||
const operator = inclusive ? 'mindestens' : 'mehr als';
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return `Text muss ${operator} ${minimum} Zeichen lang sein`;
|
||||
case 'number':
|
||||
case 'bigint':
|
||||
return `Wert muss ${operator} ${minimum} sein`;
|
||||
case 'array':
|
||||
return `Liste muss ${operator} ${minimum} Elemente enthalten`;
|
||||
case 'set':
|
||||
return `Set muss ${operator} ${minimum} Elemente enthalten`;
|
||||
case 'date':
|
||||
const minDate = typeof minimum === 'bigint' ? new Date(Number(minimum)) : new Date(minimum);
|
||||
return `Datum muss ${operator} ${minDate.toLocaleDateString('de-DE')} sein`;
|
||||
default:
|
||||
return `Wert ist zu klein (min: ${minimum})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats "too big" error messages based on the type.
|
||||
*/
|
||||
function formatTooBigMessage(issue: any): string {
|
||||
const { type, maximum, inclusive } = issue;
|
||||
const operator = inclusive ? 'höchstens' : 'weniger als';
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return `Text darf ${operator} ${maximum} Zeichen lang sein`;
|
||||
case 'number':
|
||||
case 'bigint':
|
||||
return `Wert darf ${operator} ${maximum} sein`;
|
||||
case 'array':
|
||||
return `Liste darf ${operator} ${maximum} Elemente enthalten`;
|
||||
case 'set':
|
||||
return `Set darf ${operator} ${maximum} Elemente enthalten`;
|
||||
case 'date':
|
||||
const maxDate = typeof maximum === 'bigint' ? new Date(Number(maximum)) : new Date(maximum);
|
||||
return `Datum darf ${operator} ${maxDate.toLocaleDateString('de-DE')} sein`;
|
||||
default:
|
||||
return `Wert ist zu groß (max: ${maximum})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats invalid string error messages based on validation type.
|
||||
*/
|
||||
function formatInvalidStringMessage(issue: any): string {
|
||||
let validation = 'unknown';
|
||||
|
||||
if (typeof issue.validation === 'string') {
|
||||
validation = issue.validation;
|
||||
} else if (typeof issue.validation === 'object' && issue.validation) {
|
||||
if ('includes' in issue.validation) {
|
||||
validation = 'includes';
|
||||
} else if ('startsWith' in issue.validation) {
|
||||
validation = 'startsWith';
|
||||
} else if ('endsWith' in issue.validation) {
|
||||
validation = 'endsWith';
|
||||
} else {
|
||||
validation = Object.keys(issue.validation)[0] || 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
const validationMessages: Record<string, string> = {
|
||||
email: 'Ungültige E-Mail-Adresse',
|
||||
url: 'Ungültige URL',
|
||||
uuid: 'Ungültige UUID',
|
||||
cuid: 'Ungültige CUID',
|
||||
cuid2: 'Ungültige CUID2',
|
||||
ulid: 'Ungültige ULID',
|
||||
regex: 'Format entspricht nicht dem erwarteten Muster',
|
||||
datetime: 'Ungültiges Datum/Zeit-Format',
|
||||
ip: 'Ungültige IP-Adresse',
|
||||
emoji: 'Muss ein Emoji sein',
|
||||
includes: 'Text muss bestimmte Zeichen enthalten',
|
||||
startsWith: 'Text muss mit bestimmten Zeichen beginnen',
|
||||
endsWith: 'Text muss mit bestimmten Zeichen enden',
|
||||
length: 'Text hat eine ungültige Länge',
|
||||
};
|
||||
|
||||
return validationMessages[validation] || `Ungültiges Format (${validation})`;
|
||||
}
|
||||
Reference in New Issue
Block a user