/** * Data access layer for Vaiśiṣṭyāṣṭakam (VA). * * Reads from MongoDB via Prisma. * VA is a poetic work with 8 chapters (Vaiśiṣṭyas) and no intro segments. * * Collections used: * - book_meta (via Prisma BookMeta model) * - prose_chapters (via Prisma ProseChapter model) */ import { prisma } from "@/lib/db"; import type { JsonValue } from "@prisma/client/runtime/library"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const BOOK = "va"; /** Volume sentinel for books without volumes. */ const NO_VOLUME = 0; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type VaMeta = { book: string; title: string; author: string; year: number; description: string; totalChapters: number; chapters: VaChapterEntry[]; }; export type VaChapterEntry = { number: number; title: string; }; export type VaChapter = { book: string; chapter: number | null; title: string; type: string; slug?: string; paragraphs: string[]; }; /** Used in sidebar / table-of-contents navigation. */ export type VaSidebarItem = { slug: string; label: string; type: string; chapter: number | null; }; // --------------------------------------------------------------------------- // HTML sanitisation // --------------------------------------------------------------------------- function sanitizeHtml(html: string): string { let clean = html.replace(/][\s\S]*?<\/script>/gi, ""); clean = clean.replace(/][\s\S]*?<\/style>/gi, ""); clean = clean.replace(/][\s\S]*?<\/iframe>/gi, ""); clean = clean.replace( /<\/?(?:script|style|iframe|object|embed|form|input|textarea|select|button)[^>]*>/gi, "", ); clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, ""); clean = clean.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href=""'); clean = clean.replace(/href\s*=\s*'javascript:[^']*'/gi, "href=''"); return clean; } function normalizeParagraphs(paragraphs: string[] | JsonValue): string[] { let result: string[]; if (typeof paragraphs === "string") { try { const parsed = JSON.parse(paragraphs); result = Array.isArray(parsed) ? parsed : [paragraphs]; } catch { result = [paragraphs]; } } else if (Array.isArray(paragraphs)) { result = paragraphs as string[]; } else { result = []; } return result.map(sanitizeHtml); } // --------------------------------------------------------------------------- // Book meta // --------------------------------------------------------------------------- export async function getVaMeta(): Promise { const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } }); if (!meta) return null; return meta.data as unknown as VaMeta; } // --------------------------------------------------------------------------- // Chapters // --------------------------------------------------------------------------- export async function getVaChapter( chapterNum: number, ): Promise { if (chapterNum < 1 || chapterNum > 8 || !Number.isInteger(chapterNum)) { return null; } const doc = await prisma.proseChapter.findUnique({ where: { book_volume_slug: { book: BOOK, volume: NO_VOLUME, slug: String(chapterNum), }, }, }); if (!doc) return null; return { book: doc.book, chapter: doc.chapter, title: doc.title, type: doc.type, slug: doc.slug, paragraphs: normalizeParagraphs(doc.paragraphs), }; } // --------------------------------------------------------------------------- // Unified content accessor // --------------------------------------------------------------------------- export async function getVaContent( slug: string, ): Promise { const num = Number(slug); if (!Number.isNaN(num) && Number.isInteger(num) && num >= 1 && num <= 8) { return getVaChapter(num); } return null; } // --------------------------------------------------------------------------- // Sidebar navigation // --------------------------------------------------------------------------- export async function getVaSidebarItems(): Promise { const meta = await getVaMeta(); if (!meta) return []; return meta.chapters.map((ch) => ({ slug: String(ch.number), label: `${ch.number}. ${ch.title}`, type: "chapter", chapter: ch.number, })); }