refactor(types): strengthen Zod schemas with regex validation and derive CalendarEvent

Add regex-based data URL validation for images, compute binary size
from base64 for accurate 10MB limit, enforce datetime strings with
offset for start/end fields, and derive CalendarEvent from the AI
response item type to eliminate field duplication.
This commit is contained in:
2026-04-07 13:11:05 -04:00
parent dc4204a740
commit 7bb4f2be9d

View File

@@ -1,12 +1,26 @@
import { z } from "zod"; import { z } from "zod";
import { MAX_IMAGE_SIZE_BYTES } from "@/lib/constants";
/** Validates that a base64 data URL string decodes to binary under the max size. */
const isValidImageSize = (val: string | undefined): boolean => {
if (!val) return true;
const base64Part = val.split(",")[1] ?? "";
const binarySize = Math.ceil(base64Part.length * 0.75);
return binarySize <= MAX_IMAGE_SIZE_BYTES;
};
export const AiEventRequestSchema = z export const AiEventRequestSchema = z
.object({ .object({
prompt: z.string().trim().max(2000).optional(), prompt: z.string().trim().max(2000).optional(),
imageBase64: z imageBase64: z
.string() .string()
.startsWith("data:", "Must be a valid data URL") .regex(
.max(10 * 1024 * 1024 * 1.37, "Image must be less than 10MB") /^data:image\/(png|jpeg|webp);base64,/,
"Must be a valid image data URL (PNG, JPEG, or WebP)",
)
.refine(isValidImageSize, {
message: "Image must be less than 10MB",
})
.optional(), .optional(),
}) })
.refine((data) => data.prompt || data.imageBase64, { .refine((data) => data.prompt || data.imageBase64, {
@@ -21,25 +35,18 @@ export const AiEventResponseItemSchema = z.object({
description: z.string().optional(), description: z.string().optional(),
location: z.string().optional(), location: z.string().optional(),
url: z.string().optional(), url: z.string().optional(),
start: z.string(), start: z.string().datetime({ offset: true }),
end: z.string().optional(), end: z.string().datetime({ offset: true }).optional(),
allDay: z.boolean().optional(), allDay: z.boolean().optional(),
recurrenceRule: z.string().optional(), recurrenceRule: z.string().optional(),
}); });
export const AiEventResponseSchema = z.array(AiEventResponseItemSchema); export const AiEventResponseSchema = z.array(AiEventResponseItemSchema);
export type CalendarEvent = { export type AiEventResponseItem = z.infer<typeof AiEventResponseItemSchema>;
export type CalendarEvent = AiEventResponseItem & {
id: string; id: string;
title: string;
description?: string;
location?: string;
url?: string;
start: string;
end?: string;
allDay?: boolean;
createdAt?: string; createdAt?: string;
lastModified?: string; lastModified?: string;
recurrenceRule?: string;
}; };