1
10import { Prisma } from "@prisma/client";
11
12
13export const SLUG_INPUT_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14
15const MAX_SLUG = 50;
16
17
18export function kebabSlug(name: string): string {
19 return name
20 .toLowerCase()
21 .normalize("NFKD")
22 .replace(/[^a-z0-9]+/g, "-")
23 .replace(/^-+|-+$/g, "");
24}
25
26
27export function normalizeSlug(name: string): string {
28 return kebabSlug(name).slice(0, MAX_SLUG).replace(/-+$/g, "");
29}
30
31
38export async function nextFreeSlug(
39 base: string,
40 taken: (slugs: string[]) => Promise<Set<string>>
41): Promise<string> {
42 const clean = base || "form";
43 const first = await taken([clean]);
44 if (!first.has(clean)) return clean;
45 for (let n = 2; ; n++) {
46
47 const candidate = `${clean.slice(0, MAX_SLUG - String(n).length - 1)}-${n}`;
48 const used = await taken([candidate]);
49 if (!used.has(candidate)) return candidate;
50 if (n > 1000) throw new Error(`slug generation exhausted for ${clean}`);
51 }
52}
53
54
55export function isSlugConstraintError(err: unknown): boolean {
56 return (
57 err instanceof Prisma.PrismaClientKnownRequestError &&
58 err.code === "P2002" &&
59 (Array.isArray(err.meta?.target)
60 ? (err.meta.target as string[]).includes("slug")
61 : err.meta?.target === "slug")
62 );
63}
64