import { z } from 'zod'; import { getLogger } from './logging.js'; import { ValidationError } from './error-handling.js'; const logger = getLogger(); /** * Validate input data against a Zod schema * @param schema Zod schema to validate against * @param data Data to validate * @param entityName Name of the entity being validated (for error messages) * @returns Validated and typed data * @throws ValidationError if validation fails */ export function validateWithZod( schema: T, data: unknown, entityName: string = 'data' ): z.infer { try { return schema.parse(data); } catch (error) { if (error instanceof z.ZodError) { logger.warn(`Validation failed for ${entityName}`, { error: error.errors }); // Transform Zod validation errors into our ValidationError format const validationErrors = error.errors.map(err => { const path = err.path.join('.'); return { field: path || 'unknown', message: err.message }; }); throw new ValidationError( `Invalid ${entityName}: validation failed`, validationErrors, error ); } // If it's not a ZodError, re-throw throw error; } } /** * Common schema for time-based parameters that accept ISO dates or relative time expressions */ export const TimeStringSchema = z.string() .refine( (val) => { // Valid formats: // 1. ISO date string: 2023-01-01T00:00:00Z // 2. Relative time: -30m, -1h, -7d return ( // ISO date string /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(val) || // Relative time /^-\d+[mhdwMy]$/.test(val) ); }, { message: 'Must be an ISO date string (2023-01-01T00:00:00Z) or a relative time (-30m, -1h, -7d)' } ); /** * Schema for pagination parameters */ export const PaginationSchema = z.object({ limit: z.number().int().positive().max(1000).optional(), nextToken: z.string().optional() }); /** * Validates a value is within an allowed enum set * @param values Array of allowed values * @returns Zod schema that validates against allowed values */ export function createEnumSchema(values: readonly T[]) { return z.enum(values as [T, ...T[]]); } /** * Transform a relative time string to a Date object * @param timeString Relative time string (e.g., -30m, -1h, -7d) * @returns Date object */ export function parseRelativeTime(timeString: string): Date { const now = new Date(); // If it's an ISO date string, just return a Date object if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(timeString)) { return new Date(timeString); } // Handle relative time strings const match = timeString.match(/^-(\d+)([mhdwMy])$/); if (!match) { throw new Error(`Invalid relative time format: ${timeString}`); } const [, amountStr, unit] = match; const amount = parseInt(amountStr, 10); switch (unit) { case 'm': // minutes return new Date(now.getTime() - amount * 60 * 1000); case 'h': // hours return new Date(now.getTime() - amount * 60 * 60 * 1000); case 'd': // days return new Date(now.getTime() - amount * 24 * 60 * 60 * 1000); case 'w': // weeks return new Date(now.getTime() - amount * 7 * 24 * 60 * 60 * 1000); case 'M': { // months (approximate) const newDate = new Date(now); newDate.setMonth(now.getMonth() - amount); return newDate; } case 'y': { // years (approximate) const yearDate = new Date(now); yearDate.setFullYear(now.getFullYear() - amount); return yearDate; } default: throw new Error(`Unknown time unit: ${unit}`); } }