Little rework
Build Check / build (push) Failing after 18s
Build Check / deploy-coolify (push) Has been skipped
Build Check / push-image (push) Has been skipped

This commit is contained in:
Space-Banane
2026-07-16 23:03:42 +02:00
parent 2045e95929
commit dfbe14d284
62 changed files with 2760 additions and 2380 deletions
+113
View File
@@ -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),
};
}