/** * Data access layer for server components. * * These functions query MongoDB via Prisma and are called directly by * Next.js server components. The /api/* routes exist for external consumers * and for client-side verse-navigation fetching. */ import { prisma } from "@/lib/db"; import type { JsonValue } from "@prisma/client/runtime/library"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type BookMeta = { book: string; [key: string]: unknown; }; export type VerseRecord = { id: string; book: string; division: string | null; chapter: number; verse: number; verseEnd: number | null; reference: string; bookTitle: string; devanagari: string; bengali: string | null; transliteration: string; synonyms: { word: string; meanings: string[] }[]; translation: string; purport: string[]; }; export type VerseIndexEntry = { verse: number; verseEnd: number | null; reference: string; }; export type PaginatedVerses = { verses: VerseRecord[]; total: number; page: number; limit: number; totalPages: number; hasMore: boolean; }; export type GlossaryRecord = { id: string; term: string; definition: string; definitionHtml: string; }; // --------------------------------------------------------------------------- // Prose chapter types (for KB and other prose books) // --------------------------------------------------------------------------- export type ProseChapterRecord = { id: string; book: string; volume: number; chapter: number | null; title: string; type: string; slug: string; order: number; paragraphs: string[]; }; // --------------------------------------------------------------------------- // Book meta // --------------------------------------------------------------------------- /** * Get all book metas — each has { book, ...data } where data is the intro.json blob. */ export async function getAllBooks(): Promise { const metas = await prisma.bookMeta.findMany(); return metas.map((m) => ({ book: m.book, ...(m.data as Record), })); } /** * Get a single book's meta by slug ("bg", "sb", "cc"). */ export async function getBookMeta(book: string): Promise { const meta = await prisma.bookMeta.findUnique({ where: { book } }); if (!meta) return null; return { book: meta.book, ...(meta.data as Record) }; } // --------------------------------------------------------------------------- // Verse index (lightweight — for sidebar) // --------------------------------------------------------------------------- /** * Get the verse index for a chapter — returns only verse number, verseEnd, * and reference for every verse. This is lightweight enough to fetch all at * once even for chapters with 400+ verses (each entry is ~50 bytes). * * - BG: division is null * - SB: division is canto number as string ("1", "2", …) * - CC: division is līlā key ("adi", "madhya", "antya") */ export async function getChapterVerseIndex( book: string, division: string | null, chapter: number, ): Promise { const entries = await prisma.verse.findMany({ where: { book, division, chapter }, orderBy: { verse: "asc" }, select: { verse: true, verseEnd: true, reference: true, }, }); return entries; } // --------------------------------------------------------------------------- // Verses (paginated) // --------------------------------------------------------------------------- const DEFAULT_PAGE_SIZE = 20; /** * Get a page of verses for a given book + division + chapter. * * Returns the verses for the requested page along with pagination metadata * so the client knows whether to fetch more. */ export async function getChapterVerses( book: string, division: string | null, chapter: number, page: number = 1, limit: number = DEFAULT_PAGE_SIZE, ): Promise { const skip = (page - 1) * limit; const where = { book, division, chapter }; const [verses, total] = await Promise.all([ prisma.verse.findMany({ where, orderBy: { verse: "asc" }, skip, take: limit, }), prisma.verse.count({ where }), ]); return { verses: verses as unknown as VerseRecord[], total, page, limit, totalPages: Math.ceil(total / limit), hasMore: skip + verses.length < total, }; } // --------------------------------------------------------------------------- // Single verse // --------------------------------------------------------------------------- /** * Get a single verse by its exact coordinates. */ export async function getSingleVerse( book: string, division: string | null, chapter: number, verse: number, ): Promise { const result = await prisma.verse.findFirst({ where: { book, division, chapter, verse }, }); return result as unknown as VerseRecord | null; } // --------------------------------------------------------------------------- // Prose chapters (KB and other prose books) // --------------------------------------------------------------------------- /** * Get all prose chapters/segments for a given book and volume, * optionally filtered by type ("chapter" | "intro-segment"). * Ordered by the `order` field. */ export async function getProseChapters( book: string, volume: number, type?: string, ): Promise { const where: Record = { book, volume }; if (type) where.type = type; const results = await prisma.proseChapter.findMany({ where, orderBy: { order: "asc" }, }); return results.map(normalizeProse); } /** * Get a single prose chapter by book + volume + slug. */ export async function getProseChapter( book: string, volume: number, slug: string, ): Promise { const result = await prisma.proseChapter.findUnique({ where: { book_volume_slug: { book, volume, slug }, }, }); return result ? normalizeProse(result) : null; } /** * Normalize a prose chapter from the DB. * Some chapters were stored with paragraphs as a JSON string (fallback * for BSON wire-protocol corruption). This normalizes them back to arrays. */ function normalizeProse(doc: { id: string; book: string; volume: number; chapter: number | null; title: string; type: string; slug: string; order: number; paragraphs: string[] | JsonValue; }): ProseChapterRecord { let paragraphs: string[]; if (typeof doc.paragraphs === "string") { // Was stored as stringified JSON — parse it back try { paragraphs = JSON.parse(doc.paragraphs); } catch { paragraphs = [doc.paragraphs]; } } else if (Array.isArray(doc.paragraphs)) { paragraphs = doc.paragraphs as string[]; } else { paragraphs = []; } // Sanitize HTML — strip dangerous tags and event handlers paragraphs = paragraphs.map(sanitizeHtml); return { id: doc.id, book: doc.book, volume: doc.volume, chapter: doc.chapter, title: doc.title, type: doc.type, slug: doc.slug, order: doc.order, paragraphs, }; } /** * Sanitize an HTML string by stripping dangerous tags and attributes. * Keeps safe formatting tags: , , , ,
,
, . */ function sanitizeHtml(html: string): string { // Remove , , let clean = html.replace(/][\s\S]*?<\/script>/gi, ""); clean = clean.replace(/][\s\S]*?<\/style>/gi, ""); clean = clean.replace(/][\s\S]*?<\/iframe>/gi, ""); // Remove lone