// Input parsing/validation for admin write endpoints. Each parser returns the // Prisma-ready data object or throws a ValidationError (mapped to HTTP 400). import { ProjectSize } from "@/generated/prisma/client"; export class ValidationError extends Error {} function str(value: unknown, field: string): string { if (typeof value !== "string" || value.trim() === "") { throw new ValidationError(`"${field}" is required`); } return value.trim(); } function optStr(value: unknown): string | null { if (value === undefined || value === null || value === "") return null; if (typeof value !== "string") throw new ValidationError("Expected a string"); const trimmed = value.trim(); return trimmed === "" ? null : trimmed; } function bool(value: unknown): boolean { return value === true; } function int(value: unknown): number { const n = typeof value === "number" ? value : Number.parseInt(String(value ?? 0), 10); return Number.isFinite(n) ? Math.trunc(n) : 0; } function optInt(value: unknown): number | null { if (value === undefined || value === null || value === "") return null; const n = typeof value === "number" ? value : Number.parseInt(String(value), 10); return Number.isFinite(n) ? Math.trunc(n) : null; } function tags(value: unknown, max: number): string[] { if (!Array.isArray(value)) return []; const cleaned = value .map((t) => (typeof t === "string" ? t.trim() : "")) .filter(Boolean); if (cleaned.length > max) { throw new ValidationError(`Too many tags (max ${max})`); } return cleaned; } function strList(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.map((t) => (typeof t === "string" ? t.trim() : "")).filter(Boolean); } function date(value: unknown, field: string): Date { const d = new Date(String(value)); if (Number.isNaN(d.getTime())) throw new ValidationError(`"${field}" is not a valid date`); return d; } function optDate(value: unknown): Date | null { if (value === undefined || value === null || value === "") return null; const d = new Date(String(value)); return Number.isNaN(d.getTime()) ? null : d; } export function parseProject(body: Record) { const size = String(body.size); const validSize = (Object.values(ProjectSize) as string[]).includes(size) ? (size as ProjectSize) : ProjectSize.MediumSized; return { name: str(body.name, "name"), label: str(body.label, "label"), size: validSize, imageUrl: optStr(body.imageUrl), link: str(body.link, "link"), linkIsDemo: bool(body.linkIsDemo), sourceCode: optStr(body.sourceCode), description: str(body.description, "description"), why: str(body.why, "why"), note: optStr(body.note), tags: tags(body.tags, 3), loc: optInt(body.loc), locEndpoint: optStr(body.locEndpoint), }; } export function parseExperience(body: Record) { return { company: str(body.company, "company"), role: str(body.role, "role"), fromDate: date(body.fromDate, "fromDate"), toDate: optDate(body.toDate), url: optStr(body.url), iconUrl: optStr(body.iconUrl), summary: str(body.summary, "summary"), tags: tags(body.tags, 6), sortIndex: int(body.sortIndex), }; } export function parseAffiliate(body: Record) { return { name: str(body.name, "name"), link: str(body.link, "link"), icon: str(body.icon, "icon"), location: str(body.location, "location"), provides: strList(body.provides), good: strList(body.good), bad: strList(body.bad), sortIndex: int(body.sortIndex), }; } function slug(value: unknown): string { const normalized = str(value, "slug") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); if (!normalized) { throw new ValidationError('"slug" is invalid'); } return normalized; } export function parseBlogPost(body: Record) { const isPublished = bool(body.isPublished); const publishedAt = optDate(body.publishedAt); return { title: str(body.title, "title"), slug: slug(body.slug), excerpt: str(body.excerpt, "excerpt"), coverImageUrl: optStr(body.coverImageUrl), content: str(body.content, "content"), isPublished, publishedAt: isPublished ? publishedAt ?? new Date() : publishedAt, }; }