/** * Data access layer for Two Essays (TE). * * Reads from MongoDB via Prisma. * TE is a collection of 2 standalone essays using the `essays` collection * (not `prose_chapters`). Essays are grouped by `group: "two-essays"`. * * Collections used: * - book_meta (via Prisma BookMeta model) * - essays (via Prisma Essay model) */ import { prisma } from "@/lib/db"; import type { JsonValue } from "@prisma/client/runtime/library"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const GROUP = "two-essays"; const BOOK = "te"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type TeMeta = { group: string; title: string; author: string; year: number; description: string; essays: TeEssayEntry[]; }; export type TeEssayEntry = { slug: string; title: string; order: number; }; export type TeEssay = { slug: string; title: string; author: string; year: number | null; group: string | null; order: number; paragraphs: string[]; }; export type TeSidebarItem = { slug: string; label: string; }; // --------------------------------------------------------------------------- // HTML sanitisation (mirrors lib/ej.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); } // --------------------------------------------------------------------------- // Book meta // --------------------------------------------------------------------------- /** * Book-level metadata (title, author, essay list, etc.) */ export async function getTeMeta(): Promise { const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } }); if (!meta) return null; return meta.data as unknown as TeMeta; } // --------------------------------------------------------------------------- // Essays // --------------------------------------------------------------------------- /** * All essays in the "two-essays" group, ordered by `order`. */ export async function getTeEssays(): Promise { const results = await prisma.essay.findMany({ where: { group: GROUP }, orderBy: { order: "asc" }, }); return results.map((doc) => ({ slug: doc.slug, title: doc.title, author: doc.author, year: doc.year, group: doc.group, order: doc.order, paragraphs: normalizeParagraphs(doc.paragraphs), })); } /** * A single essay by slug. */ export async function getTeEssay(slug: string): Promise { const doc = await prisma.essay.findUnique({ where: { slug } }); if (!doc) return null; return { slug: doc.slug, title: doc.title, author: doc.author, year: doc.year, group: doc.group, order: doc.order, paragraphs: normalizeParagraphs(doc.paragraphs), }; } // --------------------------------------------------------------------------- // Sidebar navigation // --------------------------------------------------------------------------- /** * Build the sidebar navigation list from meta. */ export async function getTeSidebarItems(): Promise { const meta = await getTeMeta(); if (!meta) return []; return meta.essays.map((e) => ({ slug: e.slug, label: e.title, })); }