initial commit
This commit is contained in:
103
lib/validate.ts
Normal file
103
lib/validate.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/* Pure helpers shared by the editor (client) and the API (server). */
|
||||
import type { GalleryImage, PostInput } from "./types";
|
||||
|
||||
export const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
export const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
/** "Hello, World!" -> "hello-world" */
|
||||
export function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize("NFKD")
|
||||
.replace(/[̀-ͯ]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
/** Same as categoryLabel() in portfolio/data/content.ts. */
|
||||
export function categoryLabel(slug: string): string {
|
||||
return slug
|
||||
.split(/[-_]+/)
|
||||
.filter(Boolean)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/** Trim, drop empties, dedupe case-insensitively (first spelling wins). */
|
||||
export function normalizeTags(tags: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const raw of tags) {
|
||||
const tag = raw.trim();
|
||||
const key = tag.toLowerCase();
|
||||
if (!tag || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push(tag);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function todayIso(): string {
|
||||
const d = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
}
|
||||
|
||||
function cleanImage(raw: unknown): GalleryImage | null {
|
||||
const obj = typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : null;
|
||||
const src = typeof raw === "string" ? raw : obj?.src;
|
||||
if (typeof src !== "string") return null;
|
||||
const cleaned = src.trim().replace(/^\/+/, "");
|
||||
if (!cleaned || cleaned.split("/").includes("..")) return null;
|
||||
const description = typeof obj?.description === "string" ? obj.description.trim() : "";
|
||||
return description ? { src: cleaned, description } : cleaned;
|
||||
}
|
||||
|
||||
/** Validates and normalizes an editor submission. */
|
||||
export function validatePost(raw: unknown): { post: PostInput | null; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
const r = (raw ?? {}) as Record<string, unknown>;
|
||||
const str = (key: string) => (typeof r[key] === "string" ? (r[key] as string).trim() : "");
|
||||
|
||||
const slug = str("slug");
|
||||
const title = str("title");
|
||||
const description = str("description");
|
||||
const date = str("date");
|
||||
const category = str("category");
|
||||
const details = typeof r.details === "string" ? r.details : "";
|
||||
|
||||
if (!SLUG_RE.test(slug)) errors.push("slug must be lowercase kebab-case (a-z, 0-9, -)");
|
||||
if (!title) errors.push("title is required");
|
||||
if (!description) errors.push("description is required");
|
||||
if (!DATE_RE.test(date) || Number.isNaN(Date.parse(date))) errors.push("date must be YYYY-MM-DD");
|
||||
if (!SLUG_RE.test(category)) errors.push("category must be lowercase kebab-case");
|
||||
if (!Array.isArray(r.tags) || r.tags.some((t) => typeof t !== "string")) {
|
||||
errors.push("tags must be a list of strings");
|
||||
}
|
||||
|
||||
const images: GalleryImage[] = [];
|
||||
if (r.images !== undefined) {
|
||||
if (!Array.isArray(r.images)) errors.push("images must be a list");
|
||||
else
|
||||
for (const item of r.images) {
|
||||
const img = cleanImage(item);
|
||||
if (img) images.push(img);
|
||||
else errors.push("images contains an invalid path");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) return { post: null, errors };
|
||||
return {
|
||||
post: {
|
||||
slug,
|
||||
title,
|
||||
description,
|
||||
date,
|
||||
category,
|
||||
tags: normalizeTags(r.tags as string[]),
|
||||
...(images.length ? { images } : {}),
|
||||
details: !details || details.endsWith("\n") ? details : details + "\n",
|
||||
},
|
||||
errors,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user