Little rework
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
// 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<string, unknown>) {
|
||||
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<string, unknown>) {
|
||||
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<string, unknown>) {
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user