mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
924 lines
23 KiB
Markdown
924 lines
23 KiB
Markdown
# @isa/utils/z-safe-parse
|
|
|
|
A lightweight Zod utility library for safe parsing with automatic fallback to original values on validation failures.
|
|
|
|
## Overview
|
|
|
|
The Z-Safe-Parse library provides a single utility function `safeParse()` that wraps Zod's `safeParse()` method to provide graceful error handling. Unlike Zod's standard parsing which throws errors on validation failures, this utility automatically falls back to the original input value and logs a warning to the console. This makes it ideal for scenarios where you want to validate data transformations but maintain application stability even when validation fails.
|
|
|
|
## Table of Contents
|
|
|
|
- [Features](#features)
|
|
- [Quick Start](#quick-start)
|
|
- [Core Concepts](#core-concepts)
|
|
- [API Reference](#api-reference)
|
|
- [Usage Examples](#usage-examples)
|
|
- [Error Handling](#error-handling)
|
|
- [Best Practices](#best-practices)
|
|
- [Testing](#testing)
|
|
- [Architecture Notes](#architecture-notes)
|
|
|
|
## Features
|
|
|
|
- **Safe parsing** - Never throws errors, always returns a value
|
|
- **Automatic fallback** - Returns original input when validation fails
|
|
- **Console warnings** - Logs validation errors for debugging
|
|
- **Type-safe** - Full TypeScript type inference from Zod schemas
|
|
- **Lightweight** - Single utility function with minimal dependencies
|
|
- **Zod integration** - Works seamlessly with existing Zod schemas
|
|
- **Zero configuration** - Drop-in replacement for Zod's parse method
|
|
|
|
## Quick Start
|
|
|
|
### 1. Import the Utility
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
```
|
|
|
|
### 2. Define a Zod Schema
|
|
|
|
```typescript
|
|
const UserSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
email: z.string().email(),
|
|
});
|
|
|
|
type User = z.infer<typeof UserSchema>;
|
|
```
|
|
|
|
### 3. Parse Data Safely
|
|
|
|
```typescript
|
|
// Valid data - parses successfully
|
|
const validUser = safeParse(UserSchema, {
|
|
id: 1,
|
|
name: 'John Doe',
|
|
email: 'john@example.com'
|
|
});
|
|
// Result: { id: 1, name: 'John Doe', email: 'john@example.com' }
|
|
|
|
// Invalid data - returns original input and logs warning
|
|
const invalidUser = safeParse(UserSchema, {
|
|
id: '1', // Wrong type (should be number)
|
|
name: 'Jane Doe',
|
|
email: 'invalid' // Invalid email format
|
|
});
|
|
// Result: { id: '1', name: 'Jane Doe', email: 'invalid' } (original input)
|
|
// Console: Warning logged with Zod error details
|
|
```
|
|
|
|
## Core Concepts
|
|
|
|
### Safe Parsing Philosophy
|
|
|
|
The `safeParse()` function follows a non-throwing error handling philosophy:
|
|
|
|
```typescript
|
|
// Traditional Zod parsing (throws on error)
|
|
try {
|
|
const data = UserSchema.parse(input);
|
|
// Use data...
|
|
} catch (error) {
|
|
// Handle error...
|
|
console.error('Validation failed:', error);
|
|
}
|
|
|
|
// Safe parsing (never throws)
|
|
const data = safeParse(UserSchema, input);
|
|
// Always get a value back (validated or original)
|
|
// Errors automatically logged to console
|
|
```
|
|
|
|
### Type Inference
|
|
|
|
Full TypeScript type inference is maintained:
|
|
|
|
```typescript
|
|
const UserSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
});
|
|
|
|
// TypeScript infers return type as:
|
|
// { id: number; name: string }
|
|
const user = safeParse(UserSchema, input);
|
|
```
|
|
|
|
**Important**: Due to fallback behavior, the actual runtime type might not match the inferred type if validation fails. Use this utility only when you can tolerate type mismatches or have additional type guards.
|
|
|
|
### Console Warning Output
|
|
|
|
When validation fails, a warning is logged with full error details:
|
|
|
|
```typescript
|
|
safeParse(UserSchema, { id: 'invalid', name: 123 });
|
|
|
|
// Console output:
|
|
// Warning: Failed to parse data
|
|
// ZodError: [
|
|
// {
|
|
// "code": "invalid_type",
|
|
// "expected": "number",
|
|
// "received": "string",
|
|
// "path": ["id"],
|
|
// "message": "Expected number, received string"
|
|
// },
|
|
// {
|
|
// "code": "invalid_type",
|
|
// "expected": "string",
|
|
// "received": "number",
|
|
// "path": ["name"],
|
|
// "message": "Expected string, received number"
|
|
// }
|
|
// ]
|
|
```
|
|
|
|
### Use Case Scenarios
|
|
|
|
**Ideal for**:
|
|
- Parsing external API responses where validation failures should not crash the app
|
|
- Data migrations where you want to log issues but maintain compatibility
|
|
- Form data transformations with lenient error handling
|
|
- Development/debugging scenarios where you want to see invalid data
|
|
|
|
**Not ideal for**:
|
|
- Critical data validation where failures must halt execution
|
|
- Security-sensitive parsing where invalid data is dangerous
|
|
- Type-strict contexts where runtime type mismatches cause downstream errors
|
|
|
|
## API Reference
|
|
|
|
### `safeParse<T>(schema, data): T`
|
|
|
|
Safely parses data using a Zod schema, returning validated data on success or original data on failure.
|
|
|
|
#### Parameters
|
|
|
|
| Parameter | Type | Description |
|
|
|-----------|------|-------------|
|
|
| `schema` | `z.ZodSchema<T>` | Zod schema to validate against |
|
|
| `data` | `unknown` | Data to parse and validate |
|
|
|
|
#### Returns
|
|
|
|
`T` - Validated data if parsing succeeds, otherwise the original input cast to `T`
|
|
|
|
#### Type Parameters
|
|
|
|
- `T` - The TypeScript type inferred from the Zod schema
|
|
|
|
#### Side Effects
|
|
|
|
- Logs warning to console if parsing fails (includes full Zod error)
|
|
|
|
#### Example
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
|
|
const ProductSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
price: z.number().positive(),
|
|
inStock: z.boolean()
|
|
});
|
|
|
|
type Product = z.infer<typeof ProductSchema>;
|
|
|
|
// Success case
|
|
const product: Product = safeParse(ProductSchema, {
|
|
id: 42,
|
|
name: 'Widget',
|
|
price: 19.99,
|
|
inStock: true
|
|
});
|
|
|
|
console.log(product.id); // 42 (type-safe)
|
|
|
|
// Failure case
|
|
const invalidProduct: Product = safeParse(ProductSchema, {
|
|
id: '42', // Wrong type
|
|
name: 'Widget',
|
|
price: -10, // Negative price
|
|
inStock: 'yes' // Wrong type
|
|
});
|
|
|
|
// Warning logged to console
|
|
// Returns: { id: '42', name: 'Widget', price: -10, inStock: 'yes' }
|
|
// Note: TypeScript type is Product, but runtime type doesn't match!
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### API Response Parsing
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
import { HttpClient } from '@angular/common/http';
|
|
|
|
const ApiResponseSchema = z.object({
|
|
data: z.array(z.object({
|
|
id: z.number(),
|
|
title: z.string(),
|
|
createdAt: z.string().datetime()
|
|
})),
|
|
total: z.number(),
|
|
page: z.number()
|
|
});
|
|
|
|
type ApiResponse = z.infer<typeof ApiResponseSchema>;
|
|
|
|
@Injectable()
|
|
export class DataService {
|
|
#http = inject(HttpClient);
|
|
|
|
async fetchData(): Promise<ApiResponse> {
|
|
const response = await fetch('/api/data');
|
|
const json = await response.json();
|
|
|
|
// Safe parse - won't throw even if API response is malformed
|
|
return safeParse(ApiResponseSchema, json);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Form Data Transformation
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
|
|
const FormDataSchema = z.object({
|
|
name: z.string().min(1),
|
|
age: z.coerce.number().int().positive(),
|
|
email: z.string().email(),
|
|
agreeToTerms: z.boolean()
|
|
});
|
|
|
|
type FormData = z.infer<typeof FormDataSchema>;
|
|
|
|
function processFormSubmission(rawFormData: unknown): FormData {
|
|
// Parse form data with automatic type coercion
|
|
const formData = safeParse(FormDataSchema, rawFormData);
|
|
|
|
// If validation failed, formData contains original (possibly invalid) data
|
|
// but function continues execution
|
|
console.log('Processing form:', formData);
|
|
|
|
return formData;
|
|
}
|
|
|
|
// Example usage
|
|
const submittedData = {
|
|
name: 'John',
|
|
age: '25', // String, will be coerced to number
|
|
email: 'john@example.com',
|
|
agreeToTerms: true
|
|
};
|
|
|
|
const result = processFormSubmission(submittedData);
|
|
// Success: { name: 'John', age: 25, email: 'john@example.com', agreeToTerms: true }
|
|
```
|
|
|
|
### Data Migration with Logging
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
|
|
// Old data format
|
|
interface LegacyUser {
|
|
user_id: string;
|
|
user_name: string;
|
|
user_email: string;
|
|
}
|
|
|
|
// New data format
|
|
const ModernUserSchema = z.object({
|
|
id: z.coerce.number(),
|
|
name: z.string(),
|
|
email: z.string().email()
|
|
});
|
|
|
|
type ModernUser = z.infer<typeof ModernUserSchema>;
|
|
|
|
function migrateLegacyUsers(legacyUsers: LegacyUser[]): ModernUser[] {
|
|
return legacyUsers.map(legacy => {
|
|
// Transform to new format
|
|
const transformed = {
|
|
id: legacy.user_id,
|
|
name: legacy.user_name,
|
|
email: legacy.user_email
|
|
};
|
|
|
|
// Safe parse - logs warnings for invalid data but continues migration
|
|
return safeParse(ModernUserSchema, transformed);
|
|
});
|
|
}
|
|
|
|
// Example usage
|
|
const legacyData: LegacyUser[] = [
|
|
{ user_id: '1', user_name: 'Alice', user_email: 'alice@example.com' },
|
|
{ user_id: 'invalid', user_name: 'Bob', user_email: 'not-an-email' } // Invalid!
|
|
];
|
|
|
|
const migratedUsers = migrateLegacyUsers(legacyData);
|
|
// Migrates all users, logs warnings for invalid entries
|
|
// Result: [
|
|
// { id: 1, name: 'Alice', email: 'alice@example.com' }, // Valid
|
|
// { id: 'invalid', name: 'Bob', email: 'not-an-email' } // Invalid, logged
|
|
// ]
|
|
```
|
|
|
|
### Configuration File Parsing
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
|
|
const ConfigSchema = z.object({
|
|
apiUrl: z.string().url(),
|
|
timeout: z.number().int().positive().default(5000),
|
|
retries: z.number().int().nonnegative().default(3),
|
|
debug: z.boolean().default(false)
|
|
});
|
|
|
|
type Config = z.infer<typeof ConfigSchema>;
|
|
|
|
function loadConfig(configFile: unknown): Config {
|
|
// Parse config with defaults and validation
|
|
const config = safeParse(ConfigSchema, configFile);
|
|
|
|
// Even if validation fails, app continues with original values
|
|
// (might have missing required fields!)
|
|
return config;
|
|
}
|
|
|
|
// Example usage
|
|
const userConfig = {
|
|
apiUrl: 'https://api.example.com',
|
|
timeout: '10000', // String, will be coerced to number
|
|
// retries: omitted, will use default
|
|
debug: true
|
|
};
|
|
|
|
const config = loadConfig(userConfig);
|
|
console.log(config.timeout); // 10000 (coerced from string)
|
|
console.log(config.retries); // 3 (default value)
|
|
```
|
|
|
|
### Array Validation with Filtering
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
|
|
const ItemSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
price: z.number().positive()
|
|
});
|
|
|
|
const ItemsArraySchema = z.array(ItemSchema);
|
|
|
|
type Item = z.infer<typeof ItemSchema>;
|
|
|
|
function parseItemsArray(data: unknown): Item[] {
|
|
// Parse array - if validation fails, returns original array
|
|
const items = safeParse(ItemsArraySchema, data);
|
|
|
|
// Additional runtime validation (since safeParse might return invalid data)
|
|
return items.filter((item: any) => {
|
|
return typeof item.id === 'number' &&
|
|
typeof item.name === 'string' &&
|
|
typeof item.price === 'number' &&
|
|
item.price > 0;
|
|
});
|
|
}
|
|
|
|
// Example usage
|
|
const mixedData = [
|
|
{ id: 1, name: 'Item 1', price: 10.00 }, // Valid
|
|
{ id: '2', name: 'Item 2', price: 20.00 }, // Invalid id type
|
|
{ id: 3, name: 'Item 3', price: -5 }, // Invalid price
|
|
{ id: 4, name: 'Item 4', price: 15.00 } // Valid
|
|
];
|
|
|
|
const validItems = parseItemsArray(mixedData);
|
|
// Result: Only items 1 and 4 (valid items after filtering)
|
|
// Console: Warning logged for validation failure
|
|
```
|
|
|
|
### Optional Field Handling
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
|
|
const UserProfileSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
bio: z.string().optional(),
|
|
website: z.string().url().optional(),
|
|
age: z.number().int().positive().optional()
|
|
});
|
|
|
|
type UserProfile = z.infer<typeof UserProfileSchema>;
|
|
|
|
function parseUserProfile(data: unknown): UserProfile {
|
|
return safeParse(UserProfileSchema, data);
|
|
}
|
|
|
|
// Example usage
|
|
const profile1 = parseUserProfile({
|
|
id: 1,
|
|
name: 'John',
|
|
bio: 'Software developer',
|
|
// website omitted - valid (optional)
|
|
age: 30
|
|
});
|
|
// Success: All optional fields handled correctly
|
|
|
|
const profile2 = parseUserProfile({
|
|
id: '2', // Wrong type
|
|
name: 'Jane',
|
|
website: 'not-a-url' // Invalid URL
|
|
});
|
|
// Warning logged, returns original data
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Console Warning Format
|
|
|
|
When validation fails, `safeParse()` logs a warning with this format:
|
|
|
|
```
|
|
console.warn('Failed to parse data', zodError);
|
|
```
|
|
|
|
The `zodError` object contains:
|
|
- **issues**: Array of validation errors
|
|
- **path**: Path to the failing field
|
|
- **message**: Human-readable error message
|
|
- **code**: Error code (e.g., 'invalid_type', 'too_small')
|
|
|
|
### Detecting Parse Failures
|
|
|
|
Since `safeParse()` always returns a value, you cannot rely on try/catch. To detect failures:
|
|
|
|
#### Option 1: Use Zod's safeParse directly
|
|
|
|
```typescript
|
|
const result = UserSchema.safeParse(data);
|
|
|
|
if (!result.success) {
|
|
// Handle validation failure
|
|
console.error('Validation failed:', result.error);
|
|
// Use original data or default value
|
|
return data as User;
|
|
} else {
|
|
// Use validated data
|
|
return result.data;
|
|
}
|
|
```
|
|
|
|
#### Option 2: Add runtime type guards
|
|
|
|
```typescript
|
|
const user = safeParse(UserSchema, data);
|
|
|
|
// Runtime check
|
|
if (typeof user.id !== 'number') {
|
|
// Validation likely failed
|
|
console.error('Invalid user data, using defaults');
|
|
return getDefaultUser();
|
|
}
|
|
|
|
return user;
|
|
```
|
|
|
|
#### Option 3: Combine with validation flag
|
|
|
|
```typescript
|
|
function parseWithValidation<T>(
|
|
schema: z.ZodSchema<T>,
|
|
data: unknown
|
|
): { data: T; isValid: boolean } {
|
|
const result = schema.safeParse(data);
|
|
|
|
if (result.success) {
|
|
return { data: result.data, isValid: true };
|
|
} else {
|
|
console.warn('Failed to parse data', result.error);
|
|
return { data: data as T, isValid: false };
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const { data, isValid } = parseWithValidation(UserSchema, input);
|
|
|
|
if (!isValid) {
|
|
// Handle invalid data
|
|
return getDefaultUser();
|
|
}
|
|
|
|
return data;
|
|
```
|
|
|
|
### Production Considerations
|
|
|
|
For production environments, consider:
|
|
|
|
1. **Custom logging** - Replace `console.warn` with proper logging service
|
|
2. **Error tracking** - Send validation errors to error tracking (Sentry, etc.)
|
|
3. **Metrics** - Track validation failure rates
|
|
4. **Alerts** - Alert on high validation failure rates
|
|
|
|
```typescript
|
|
// Production-ready wrapper
|
|
import { safeParse as baseSafeParse } from '@isa/utils/z-safe-parse';
|
|
import { z } from 'zod';
|
|
import { LoggingService } from '@isa/core/logging';
|
|
|
|
export function safeParse<T>(
|
|
schema: z.ZodSchema<T>,
|
|
data: unknown,
|
|
context?: string
|
|
): T {
|
|
const result = schema.safeParse(data);
|
|
|
|
if (!result.success) {
|
|
// Log to proper logging service
|
|
inject(LoggingService).warn('Validation failed', {
|
|
context,
|
|
error: result.error,
|
|
data
|
|
});
|
|
|
|
// Track metrics
|
|
trackValidationFailure(schema, context);
|
|
}
|
|
|
|
return result.success ? result.data : (data as T);
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### When to Use safeParse()
|
|
|
|
**Use safeParse() when**:
|
|
- Parsing external data that might be malformed
|
|
- Development/debugging to see invalid data
|
|
- Data migrations where errors should be logged but not halt execution
|
|
- Lenient form validation where UX requires showing original input
|
|
|
|
**Don't use safeParse() when**:
|
|
- Validation failure should stop execution
|
|
- Type safety is critical for downstream code
|
|
- Security-sensitive data parsing
|
|
- You need to differentiate between valid and invalid data programmatically
|
|
|
|
### Type Safety Considerations
|
|
|
|
```typescript
|
|
// Risky - runtime type might not match TypeScript type
|
|
const user: User = safeParse(UserSchema, input);
|
|
user.id.toFixed(2); // Might fail if id is actually a string!
|
|
|
|
// Safer - add runtime guards
|
|
const user = safeParse(UserSchema, input);
|
|
if (typeof user.id === 'number') {
|
|
user.id.toFixed(2); // Safe
|
|
}
|
|
|
|
// Safest - use Zod's safeParse directly for critical paths
|
|
const result = UserSchema.safeParse(input);
|
|
if (result.success) {
|
|
result.data.id.toFixed(2); // Type-safe
|
|
}
|
|
```
|
|
|
|
### Logging Best Practices
|
|
|
|
```typescript
|
|
// Bad - Silent failures
|
|
const data = safeParse(schema, input);
|
|
// No way to know if parsing failed!
|
|
|
|
// Good - Check console for warnings during development
|
|
const data = safeParse(schema, input);
|
|
// Warnings appear in console during development
|
|
|
|
// Better - Use proper logging in production
|
|
const result = schema.safeParse(input);
|
|
if (!result.success) {
|
|
logger.warn('Validation failed', { error: result.error, input });
|
|
}
|
|
return result.success ? result.data : (input as T);
|
|
```
|
|
|
|
### Schema Design
|
|
|
|
Design schemas to be lenient when using safeParse():
|
|
|
|
```typescript
|
|
// Too strict - many false negatives
|
|
const StrictSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string().min(1).max(50),
|
|
email: z.string().email(),
|
|
age: z.number().int().min(18).max(120)
|
|
});
|
|
|
|
// More lenient - graceful degradation
|
|
const LenientSchema = z.object({
|
|
id: z.coerce.number(), // Coerce from string
|
|
name: z.string().default(''), // Default for missing values
|
|
email: z.string().optional(), // Optional instead of required
|
|
age: z.coerce.number().optional() // Coerce + optional
|
|
});
|
|
```
|
|
|
|
### Combining with Other Validation
|
|
|
|
```typescript
|
|
import { safeParse } from '@isa/utils/z-safe-parse';
|
|
|
|
// Zod for structure validation
|
|
const UserSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
email: z.string()
|
|
});
|
|
|
|
// Custom business logic validation
|
|
function validateUserBusinessRules(user: User): boolean {
|
|
// Check business rules that Zod can't express
|
|
return user.name !== 'admin' && user.email.includes('@');
|
|
}
|
|
|
|
// Combined validation
|
|
function parseAndValidateUser(data: unknown): User | null {
|
|
const user = safeParse(UserSchema, data);
|
|
|
|
if (!validateUserBusinessRules(user)) {
|
|
console.error('Business validation failed');
|
|
return null;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
The library uses **Jest** for testing.
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# Run tests for this library
|
|
npx nx test utils-z-safe-parse --skip-nx-cache
|
|
|
|
# Run tests with coverage
|
|
npx nx test utils-z-safe-parse --code-coverage --skip-nx-cache
|
|
|
|
# Run tests in watch mode
|
|
npx nx test utils-z-safe-parse --watch
|
|
```
|
|
|
|
### Test Examples
|
|
|
|
```typescript
|
|
import { describe, it, expect, vi } from '@jest/globals';
|
|
import { safeParse } from './utils-z-safe-parse';
|
|
import { z } from 'zod';
|
|
|
|
describe('safeParse', () => {
|
|
const TestSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string()
|
|
});
|
|
|
|
it('should return parsed data for valid input', () => {
|
|
const input = { id: 1, name: 'Test' };
|
|
const result = safeParse(TestSchema, input);
|
|
|
|
expect(result).toEqual(input);
|
|
});
|
|
|
|
it('should return original data for invalid input', () => {
|
|
const input = { id: 'invalid', name: 123 };
|
|
const result = safeParse(TestSchema, input);
|
|
|
|
expect(result).toEqual(input);
|
|
});
|
|
|
|
it('should log warning for invalid input', () => {
|
|
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation();
|
|
|
|
const input = { id: 'invalid', name: 'Test' };
|
|
safeParse(TestSchema, input);
|
|
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
'Failed to parse data',
|
|
expect.any(Object)
|
|
);
|
|
|
|
consoleWarnSpy.mockRestore();
|
|
});
|
|
|
|
it('should handle complex nested schemas', () => {
|
|
const NestedSchema = z.object({
|
|
user: z.object({
|
|
id: z.number(),
|
|
profile: z.object({
|
|
name: z.string(),
|
|
age: z.number()
|
|
})
|
|
})
|
|
});
|
|
|
|
const validInput = {
|
|
user: {
|
|
id: 1,
|
|
profile: { name: 'John', age: 30 }
|
|
}
|
|
};
|
|
|
|
const result = safeParse(NestedSchema, validInput);
|
|
expect(result).toEqual(validInput);
|
|
});
|
|
|
|
it('should preserve type coercion', () => {
|
|
const CoerceSchema = z.object({
|
|
id: z.coerce.number(),
|
|
active: z.coerce.boolean()
|
|
});
|
|
|
|
const input = { id: '42', active: 'true' };
|
|
const result = safeParse(CoerceSchema, input);
|
|
|
|
expect(result.id).toBe(42);
|
|
expect(result.active).toBe(true);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Architecture Notes
|
|
|
|
### Implementation Details
|
|
|
|
The `safeParse()` function is a thin wrapper around Zod's `safeParse()` method:
|
|
|
|
```typescript
|
|
export function safeParse<T>(schema: z.ZodSchema<T>, data: unknown): T {
|
|
const parsed = schema.safeParse(data);
|
|
|
|
if (!parsed.success) {
|
|
console.warn('Failed to parse data', parsed.error);
|
|
return data as T;
|
|
}
|
|
|
|
return parsed.data;
|
|
}
|
|
```
|
|
|
|
**Key characteristics**:
|
|
1. **Single responsibility** - Only handles safe parsing with fallback
|
|
2. **Zero configuration** - Works out of the box
|
|
3. **Type preservation** - Maintains TypeScript types from schema
|
|
4. **Non-throwing** - Always returns a value
|
|
5. **Logging only** - Side effect limited to console.warn
|
|
|
|
### Design Decisions
|
|
|
|
#### 1. Fallback to Original Value
|
|
|
|
**Decision**: Return original input when validation fails
|
|
|
|
**Rationale**:
|
|
- Maintains application stability
|
|
- Allows graceful degradation
|
|
- Useful for development/debugging
|
|
|
|
**Tradeoff**: Type safety is compromised (TypeScript type might not match runtime type)
|
|
|
|
#### 2. Console Warning
|
|
|
|
**Decision**: Log to console.warn instead of console.error
|
|
|
|
**Rationale**:
|
|
- Warning severity (not critical error)
|
|
- Easy to find in browser dev tools
|
|
- Doesn't trigger error tracking by default
|
|
|
|
**Tradeoff**: May be missed in production without proper monitoring
|
|
|
|
#### 3. No Success Flag
|
|
|
|
**Decision**: Don't return a success boolean with the result
|
|
|
|
**Rationale**:
|
|
- Simpler API (just returns T)
|
|
- Encourages graceful handling
|
|
- Keeps wrapper minimal
|
|
|
|
**Tradeoff**: Can't programmatically detect failures without runtime checks
|
|
|
|
### Comparison with Alternatives
|
|
|
|
#### vs. Zod's parse()
|
|
|
|
```typescript
|
|
// Zod's parse() - throws on error
|
|
try {
|
|
const data = UserSchema.parse(input);
|
|
} catch (error) {
|
|
// Handle error
|
|
}
|
|
|
|
// safeParse() - never throws
|
|
const data = safeParse(UserSchema, input);
|
|
// Warning logged if invalid
|
|
```
|
|
|
|
#### vs. Zod's safeParse()
|
|
|
|
```typescript
|
|
// Zod's safeParse() - returns result object
|
|
const result = UserSchema.safeParse(input);
|
|
if (result.success) {
|
|
// Use result.data
|
|
} else {
|
|
// Handle result.error
|
|
}
|
|
|
|
// safeParse() - always returns data
|
|
const data = safeParse(UserSchema, input);
|
|
// Original data if validation fails
|
|
```
|
|
|
|
### Known Limitations
|
|
|
|
#### 1. Type Safety Compromise (High Impact)
|
|
|
|
**Limitation**: Runtime type might not match TypeScript type
|
|
|
|
**Impact**: High - can cause runtime errors in downstream code
|
|
|
|
**Mitigation**:
|
|
- Add runtime type guards for critical paths
|
|
- Use Zod's safeParse() for security-sensitive data
|
|
- Document when validation might fail
|
|
|
|
#### 2. No Programmatic Error Detection (Medium Impact)
|
|
|
|
**Limitation**: Can't distinguish valid from invalid data programmatically
|
|
|
|
**Impact**: Medium - requires runtime checks or separate validation
|
|
|
|
**Mitigation**:
|
|
- Check console for warnings during development
|
|
- Add custom wrapper that returns success flag
|
|
- Use Zod's safeParse() when you need to detect failures
|
|
|
|
#### 3. Console-Only Logging (Medium Impact)
|
|
|
|
**Limitation**: Errors only logged to console
|
|
|
|
**Impact**: Medium - no error tracking, metrics, or alerts by default
|
|
|
|
**Mitigation**:
|
|
- Wrap safeParse() with custom logging in production
|
|
- Integrate with error tracking services
|
|
- Add metrics tracking for validation failures
|
|
|
|
## Dependencies
|
|
|
|
### Required Libraries
|
|
|
|
- `zod` - Runtime schema validation
|
|
|
|
### Path Alias
|
|
|
|
Import from: `@isa/utils/z-safe-parse`
|
|
|
|
### Project Configuration
|
|
|
|
- **Project Name**: `utils-z-safe-parse`
|
|
- **Prefix**: `util`
|
|
- **Testing**: Jest
|
|
- **Source Root**: `libs/utils/z-safe-parse/src`
|
|
|
|
## License
|
|
|
|
Internal ISA Frontend library - not for external distribution.
|