/** * Data access layer for Magazines (Back to Godhead, etc.). * * Uses dedicated Prisma models: * - MagazineMeta (collection: magazine_meta) * - MagazineIssue (collection: magazine_issues) */ import { prisma } from "@/lib/db"; import type { JsonValue } from "@prisma/client/runtime/library"; import fs from "fs"; import path from "path"; const LOCAL_JSON_DIR = path.join(process.cwd(), "lib", "jsons", "magazines"); export type MagazineSubChapter = { slug: string; title: string }; export type MagazineVolumeGroup = { title: string; subChapters: MagazineSubChapter[]; }; export type MagazineChapterEntry = MagazineSubChapter | MagazineVolumeGroup; export type MagazineMetaData = { book: string; title: string; author: string; year: number | null; description: string | null; totalIssues: number; totalArticles: number; flag: string | null; chapters: MagazineChapterEntry[]; }; export type MagazineIssueData = { book: string; volume: number; part: number; title: string; slug: string; paragraphs: string[]; }; export type MagazineSummary = { book: string; title: string; author: string; year: number | null; description: string | null; totalIssues: number; flag: string | null; }; export type MagazineSidebarItem = { slug: string; label: string; type: string; }; 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); } function findLocalDirForBook(bookCode: string): string | null { if (!fs.existsSync(LOCAL_JSON_DIR)) return null; const entries = fs.readdirSync(LOCAL_JSON_DIR); for (const entry of entries) { const introPath = path.join(LOCAL_JSON_DIR, entry, "intro.json"); if (fs.existsSync(introPath)) { try { const intro = JSON.parse(fs.readFileSync(introPath, "utf-8")); if (intro.book === bookCode) return entry; } catch { /* skip */ } } } return null; } function readLocalJson(filePath: string): T | null { try { if (!fs.existsSync(filePath)) return null; return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T; } catch { return null; } } function getAllLocalMetas(): MagazineMetaData[] { if (!fs.existsSync(LOCAL_JSON_DIR)) return []; const entries = fs.readdirSync(LOCAL_JSON_DIR).sort(); const metas: MagazineMetaData[] = []; for (const entry of entries) { const intro = readLocalJson( path.join(LOCAL_JSON_DIR, entry, "intro.json"), ); if (intro) metas.push(intro); } return metas; } function getLocalMeta(bookCode: string): MagazineMetaData | null { const dir = findLocalDirForBook(bookCode); if (!dir) return null; return readLocalJson( path.join(LOCAL_JSON_DIR, dir, "intro.json"), ); } function getLocalIssue( bookCode: string, slug: string, ): MagazineIssueData | null { const dir = findLocalDirForBook(bookCode); if (!dir) return null; const data = readLocalJson( path.join(LOCAL_JSON_DIR, dir, "issues", `${slug}.json`), ); if (!data) return null; return { ...data, paragraphs: data.paragraphs.map(sanitizeHtml) }; } // Public API export async function getAllMagazines(): Promise { try { const results = await prisma.magazineMeta.findMany({ orderBy: { book: "asc" }, }); if (results.length > 0) { return results.map((row) => { const data = row.data as unknown as MagazineMetaData; return { book: row.book, title: data.title, author: data.author, year: data.year ?? null, description: data.description ?? null, totalIssues: data.totalIssues, flag: data.flag ?? null, }; }); } } catch { /* fall through */ } const localMetas = getAllLocalMetas(); return localMetas.map((meta) => ({ book: meta.book, title: meta.title, author: meta.author, year: meta.year ?? null, description: meta.description ?? null, totalIssues: meta.totalIssues, flag: meta.flag ?? null, })); } export async function getMagazineMeta( bookCode: string, ): Promise { try { const row = await prisma.magazineMeta.findUnique({ where: { book: bookCode }, }); if (row) return row.data as unknown as MagazineMetaData; } catch { /* fall through */ } return getLocalMeta(bookCode); } export async function getMagazineIssue( bookCode: string, slug: string, ): Promise { if (!slug) return null; try { const doc = await prisma.magazineIssue.findUnique({ where: { book_slug: { book: bookCode, slug } }, }); if (doc) { // Support both paragraphsJson (string) and paragraphs (array) formats. // The seed script stores paragraphs as a JSON string to avoid // MongoDB BSON wire protocol issues with large string arrays. let paras: string[]; if (doc.paragraphsJson) { try { paras = JSON.parse(doc.paragraphsJson); } catch { paras = []; } } else { paras = normalizeParagraphs(doc.paragraphs); } return { book: doc.book, volume: doc.volume, part: doc.part, title: doc.title, slug: doc.slug, paragraphs: paras, }; } } catch { /* fall through */ } return getLocalIssue(bookCode, slug); } export async function getMagazineContent( bookCode: string, slug: string, ): Promise { return getMagazineIssue(bookCode, slug); } function isSubChapter(ch: MagazineChapterEntry): ch is MagazineSubChapter { return "slug" in ch && !("subChapters" in ch); } function hasSubChapters(ch: MagazineChapterEntry): ch is MagazineVolumeGroup { return "subChapters" in ch && Array.isArray(ch.subChapters); } export async function getMagazineSidebarItems( bookCode: string, ): Promise { const meta = await getMagazineMeta(bookCode); if (!meta) return []; const items: MagazineSidebarItem[] = []; function addEntry(entry: MagazineChapterEntry) { if (isSubChapter(entry)) { items.push({ slug: entry.slug, label: entry.title, type: "chapter" }); } else if (hasSubChapters(entry)) { items.push({ slug: "", label: entry.title, type: "heading" }); for (const sc of entry.subChapters) { addEntry(sc); } } } for (const ch of meta.chapters) { addEntry(ch); } return items; } export function bookCodeToSlug(bookCode: string): string { return bookCode; } export function slugToBookCode(slug: string): string { return slug; }