/** * Data access layer for Śrī Brahma-saṁhitā (BS). * * Reads from MongoDB via Prisma. * BS has BOTH verses (in the `verses` collection) * AND prose intro segments (in the `prose_chapters` collection). * * Verses were seeded with: * - book: "bs", division: null * - chapter = text number (1-62) * - verse = always 1 * * Intro segments were seeded with: * - book: "bs", volume: 0, type: "intro-segment" * - slugs like "intro-introduction", "intro-foreword" */ import { prisma } from "@/lib/db"; import type { JsonValue } from "@prisma/client/runtime/library"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const BOOK = "bs"; const NO_VOLUME = 0; // --------------------------------------------------------------------------- // Text title lookup // --------------------------------------------------------------------------- const TEXT_TITLES: Record = { 1: "Text 1", 2: "Text 2", 3: "Text 3", 4: "Text 4", 5: "Text 5", 6: "Text 6", 7: "Text 7", 8: "Text 8", 9: "Text 9", 10: "Text 10", 11: "Text 11", 12: "Text 12", 13: "Text 13", 14: "Text 14", 15: "Text 15", 16: "Text 16", 17: "Text 17", 18: "Text 18", 19: "Text 19", 20: "Text 20", 21: "Text 21", 22: "Text 22", 23: "Text 23", 24: "Text 24", 25: "Text 25", 26: "Text 26", 27: "Text 27", 28: "Text 28", 29: "Text 29", 30: "Text 30", 31: "Text 31", 32: "Text 32", 33: "Text 33", 34: "Text 34", 35: "Text 35", 36: "Text 36", 37: "Text 37", 38: "Text 38", 39: "Text 39", 40: "Text 40", 41: "Text 41", 42: "Text 42", 43: "Text 43", 44: "Text 44", 45: "Text 45", 46: "Text 46", 47: "Text 47", 48: "Text 48", 49: "Text 49", 50: "Text 50", 51: "Text 51", 52: "Text 52", 53: "Text 53", 54: "Text 54", 55: "Text 55", 56: "Text 56", 57: "Text 57", 58: "Text 58", 59: "Text 59", 60: "Text 60", 61: "Text 61", 62: "Text 62", }; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type BsMeta = { book: string; title: string; author: string; year: number; description: string; totalTexts: number; texts: BsTextEntry[]; }; export type BsTextEntry = { number: number; title: string; slug: string; }; export type BsIntroSegment = { slug: string; title: string; type: string; book: string; order: number; paragraphs: string[]; }; export type BsVerse = { book: string; book_title: string; text: number; // 1-62 title: string; reference: string; devanagari: string; transliteration: string; synonyms: { word: string; meanings: string[] }[]; translation: string; purport: string[]; }; /** Union type used in sidebar / table-of-contents navigation. */ export type BsSidebarItem = { /** URL-safe slug: "intro-introduction", "intro-foreword", "1" … "62" */ slug: string; /** Human-readable label for the sidebar */ label: string; /** "intro-segment" | "verse" */ type: string; /** Text number for verses, or null for intro segments */ text: number | null; }; // --------------------------------------------------------------------------- // HTML sanitisation (mirrors lib/iso.ts) // --------------------------------------------------------------------------- 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; } /** Normalize paragraphs — handles both native arrays and stringified JSON. */ 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); } /** Normalize purport (string[] from Prisma) via normalizeParagraphs. */ function normalizePurport(purport: string[] | JsonValue): string[] { return normalizeParagraphs(purport); } // --------------------------------------------------------------------------- // Book meta // --------------------------------------------------------------------------- /** * Book-level metadata (title, author, texts list, etc.) */ export async function getBsMeta(): Promise { const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } }); if (!meta) return null; return meta.data as unknown as BsMeta; } // --------------------------------------------------------------------------- // Intro segments (from prose_chapters collection) // --------------------------------------------------------------------------- /** * All intro segments, ordered by their `order` field. */ export async function getBsIntroSegments(): Promise { const results = await prisma.proseChapter.findMany({ where: { book: BOOK, volume: NO_VOLUME, type: "intro-segment" }, orderBy: { order: "asc" }, }); return results.map((doc) => ({ slug: doc.slug, title: doc.title, type: doc.type, book: doc.book, order: doc.order, paragraphs: normalizeParagraphs(doc.paragraphs), })); } /** * A single intro segment by slug. * Accepts both "intro-introduction" (URL slug) and "introduction" (raw slug). */ export async function getBsIntroSegment( slug: string, ): Promise { // DB stores with "intro-" prefix; accept either form const dbSlug = slug.startsWith("intro-") ? slug : `intro-${slug}`; const doc = await prisma.proseChapter.findUnique({ where: { book_volume_slug: { book: BOOK, volume: NO_VOLUME, slug: dbSlug }, }, }); if (!doc) return null; return { slug: doc.slug, title: doc.title, type: doc.type, book: doc.book, order: doc.order, paragraphs: normalizeParagraphs(doc.paragraphs), }; } // --------------------------------------------------------------------------- // Verses (from verses collection) // --------------------------------------------------------------------------- /** * A single verse/text by slug. * * - "1"-"62" → chapter=1–62, verse=1 * * Verse stored as chapter=text_number, verse=1 in the verses collection. */ export async function getBsVerse(slug: string): Promise { const num = Number(slug); if (!Number.isInteger(num) || num < 1 || num > 62) return null; const textNum = num; const doc = await prisma.verse.findFirst({ where: { book: BOOK, division: null, chapter: textNum, verse: 1 }, }); if (!doc) return null; return { book: doc.book, book_title: doc.bookTitle, text: doc.chapter, // chapter field stores the text number title: TEXT_TITLES[doc.chapter] ?? `Text ${doc.chapter}`, reference: doc.reference, devanagari: doc.devanagari, transliteration: doc.transliteration, synonyms: doc.synonyms as { word: string; meanings: string[] }[], translation: sanitizeHtml(doc.translation), purport: normalizePurport(doc.purport), }; } // --------------------------------------------------------------------------- // Unified content accessor // --------------------------------------------------------------------------- /** * Get content by URL slug. * * "intro-introduction" → intro segment * "intro-foreword" → intro segment * "1" … "62" → verse (text) */ export async function getBsContent( slug: string, ): Promise { if (slug.startsWith("intro-")) { return getBsIntroSegment(slug); } const num = Number(slug); if (!Number.isNaN(num) && Number.isInteger(num) && num >= 1 && num <= 62) { return getBsVerse(String(num)); } return null; } // --------------------------------------------------------------------------- // Sidebar navigation // --------------------------------------------------------------------------- /** * Build the full sidebar navigation list. * * Order: Introduction → Foreword → Texts 1-62 */ export async function getBsSidebarItems(): Promise { const items: BsSidebarItem[] = []; // 1. Introduction intro segment items.push({ slug: "intro-introduction", label: "Introduction", type: "intro-segment", text: null, }); // 2. Foreword intro segment items.push({ slug: "intro-foreword", label: "Foreword", type: "intro-segment", text: null, }); // 3. Texts 1-62 (verses) for (let i = 1; i <= 62; i++) { items.push({ slug: String(i), label: `${i}. ${TEXT_TITLES[i]}`, type: "verse", text: i, }); } return items; }