mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
237 lines
6.7 KiB
TypeScript
237 lines
6.7 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* Common reusable Zod schemas using Zod v4 features
|
|
* These base schemas reduce repetition and improve maintainability
|
|
*/
|
|
|
|
// ============================================================================
|
|
// Base Field Schemas
|
|
// ============================================================================
|
|
|
|
/**
|
|
* UUID/ID field - used across all entities
|
|
*/
|
|
export const idSchema = z.string().min(1).describe('Unique identifier');
|
|
|
|
/**
|
|
* Name field - used across all entities
|
|
*/
|
|
export const nameSchema = z.string().min(1).describe('Name');
|
|
|
|
/**
|
|
* Optional URL field that accepts absolute URLs, relative paths, or empty string
|
|
* Uses Zod v4's improved error handling
|
|
* Accepts:
|
|
* - Absolute URLs (http://, https://)
|
|
* - Relative paths (starting with /)
|
|
* - Empty strings
|
|
* - null/undefined
|
|
*/
|
|
export const optionalUrlSchema = z
|
|
.union([
|
|
z.string().refine(
|
|
(val) => {
|
|
if (val === '') return true;
|
|
// Check if it's a valid absolute URL
|
|
try {
|
|
new URL(val);
|
|
return true;
|
|
} catch {
|
|
// If not a valid URL, check if it's a relative path
|
|
return val.startsWith('/');
|
|
}
|
|
},
|
|
{ message: 'Must be a valid URL, relative path starting with /, or empty string' }
|
|
),
|
|
z.null(),
|
|
])
|
|
.optional()
|
|
.describe('URL or path (can be absolute URL, relative path, empty string, null, or undefined)');
|
|
|
|
/**
|
|
* Coordinate validation - latitude
|
|
*/
|
|
export const latitudeSchema = z
|
|
.number()
|
|
.min(-90, { message: 'Latitude must be between -90 and 90' })
|
|
.max(90, { message: 'Latitude must be between -90 and 90' })
|
|
.describe('Latitude in decimal degrees');
|
|
|
|
/**
|
|
* Coordinate validation - longitude
|
|
*/
|
|
export const longitudeSchema = z
|
|
.number()
|
|
.min(-180, { message: 'Longitude must be between -180 and 180' })
|
|
.max(180, { message: 'Longitude must be between -180 and 180' })
|
|
.describe('Longitude in decimal degrees');
|
|
|
|
/**
|
|
* Coordinate pair schema
|
|
*/
|
|
export const coordinateSchema = z
|
|
.object({
|
|
lat: latitudeSchema,
|
|
lng: longitudeSchema,
|
|
})
|
|
.describe('Geographic coordinates');
|
|
|
|
/**
|
|
* ISO 8601 timestamp string (RFC3339 format from Go backend)
|
|
*/
|
|
export const timestampSchema = z.string().refine((val) => {
|
|
// Accept ISO datetime strings with optional timezone offset
|
|
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}:\d{2}|Z)?$/.test(val);
|
|
}).optional().describe('ISO 8601 timestamp');
|
|
|
|
/**
|
|
* Positive number schema
|
|
*/
|
|
export const positiveNumberSchema = z
|
|
.number()
|
|
.positive({ message: 'Must be a positive number' })
|
|
.describe('Positive number');
|
|
|
|
/**
|
|
* Non-negative number schema
|
|
*/
|
|
export const nonNegativeNumberSchema = z
|
|
.number()
|
|
.nonnegative({ message: 'Must be a non-negative number' })
|
|
.describe('Non-negative number');
|
|
|
|
// ============================================================================
|
|
// Base Entity Schemas (Backend-aligned)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Base schema for backend entities with common fields
|
|
* Uses Zod v4's .extend() for composition
|
|
*/
|
|
export const baseBackendEntitySchema = z.object({
|
|
ID: idSchema,
|
|
CreatedAt: timestampSchema,
|
|
UpdatedAt: timestampSchema,
|
|
});
|
|
|
|
/**
|
|
* Base schema for backend entities with name
|
|
*/
|
|
export const namedBackendEntitySchema = baseBackendEntitySchema.extend({
|
|
Name: nameSchema,
|
|
});
|
|
|
|
/**
|
|
* Base schema for request entities (snake_case)
|
|
*/
|
|
export const baseRequestEntitySchema = z.object({
|
|
id: idSchema.optional(),
|
|
});
|
|
|
|
// ============================================================================
|
|
// Common Validation Patterns
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Email validation with empty string fallback
|
|
*/
|
|
export const optionalEmailSchema = z
|
|
.string()
|
|
.email({ message: 'Please enter a valid email address' })
|
|
.or(z.literal(''))
|
|
.optional()
|
|
.describe('Email address (can be empty)');
|
|
|
|
/**
|
|
* Phone number validation (Russian format)
|
|
*/
|
|
export const phoneSchema = z
|
|
.string()
|
|
.regex(/^((\+7|7|8)+([0-9()\-\s]){10,15})?$/, {
|
|
message: 'Please enter a valid phone number',
|
|
})
|
|
.or(z.literal(''))
|
|
.optional()
|
|
.describe('Phone number (Russian format)');
|
|
|
|
/**
|
|
* Year validation (for founding year, etc.)
|
|
*/
|
|
export const yearSchema = z.coerce
|
|
.number()
|
|
.int()
|
|
.min(1800, { message: 'Please enter a valid year' })
|
|
.max(new Date().getFullYear(), { message: 'The year cannot be in the future' })
|
|
.describe('Year');
|
|
|
|
/**
|
|
* Score validation (0-1 range)
|
|
* Commonly used for compatibility scores, risk assessments, etc.
|
|
*/
|
|
export const scoreSchema = z
|
|
.number()
|
|
.min(0, { message: 'Score must be between 0 and 1' })
|
|
.max(1, { message: 'Score must be between 0 and 1' })
|
|
.describe('Score (0-1 range)');
|
|
|
|
// ============================================================================
|
|
// Schema Composition Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Create a backend entity schema with common fields
|
|
* Uses Zod v4's composition features
|
|
*/
|
|
export function createBackendEntitySchema<T extends z.ZodRawShape>(additionalFields: T) {
|
|
return baseBackendEntitySchema.extend(additionalFields);
|
|
}
|
|
|
|
/**
|
|
* Create a named backend entity schema
|
|
*/
|
|
export function createNamedBackendEntitySchema<T extends z.ZodRawShape>(additionalFields: T) {
|
|
return namedBackendEntitySchema.extend(additionalFields);
|
|
}
|
|
|
|
/**
|
|
* Create a request schema with common patterns
|
|
*/
|
|
export function createRequestSchema<T extends z.ZodRawShape>(additionalFields: T) {
|
|
return baseRequestEntitySchema.extend(additionalFields);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Coordinate Utilities
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Validate and normalize coordinates
|
|
* Returns normalized [lat, lng] or null if invalid
|
|
*/
|
|
export function validateCoordinates(lat: unknown, lng: unknown): [number, number] | null {
|
|
const result = coordinateSchema.safeParse({ lat, lng });
|
|
return result.success ? [result.data.lat, result.data.lng] : null;
|
|
}
|
|
|
|
/**
|
|
* Check if coordinates are within bounds
|
|
*/
|
|
export function areCoordinatesInBounds(
|
|
lat: number,
|
|
lng: number,
|
|
bounds: { north: number; south: number; east: number; west: number }
|
|
): boolean {
|
|
const coordResult = coordinateSchema.safeParse({ lat, lng });
|
|
if (!coordResult.success) return false;
|
|
|
|
const { lat: validLat, lng: validLng } = coordResult.data;
|
|
|
|
// Handle longitude wrapping
|
|
const lngInBounds =
|
|
(bounds.west <= bounds.east && validLng >= bounds.west && validLng <= bounds.east) ||
|
|
(bounds.west > bounds.east && (validLng >= bounds.west || validLng <= bounds.east));
|
|
|
|
return validLat >= bounds.south && validLat <= bounds.north && lngInBounds;
|
|
}
|