/** * Data access layer for Letters. * * Provides queries for browsing letters by year, recipient, and location. * Uses the Prisma Letter model (collection: letters). * * Recipients are stored as an array (e.g., ["Mukunda", "Janaki"]). * The recipient list aggregates individual names across all letters, * and searching for "Mukunda" finds all letters where Mukunda is * ANY recipient (individual or group). */ import { prisma } from "@/lib/db"; // Types export type LetterSummary = { slug: string; recipients: string[]; recipientDisplay: string; date: string | null; year: number | null; month: number | null; location: string | null; }; export type LetterFull = { slug: string; recipients: string[]; recipientDisplay: string; date: string | null; dateRef: string | null; year: number | null; month: number | null; day: number | null; location: string | null; salutation: string | null; paragraphs: string[]; }; export type YearGroup = { year: number; count: number; }; export type RecipientGroup = { name: string; count: number; }; export type LocationGroup = { name: string; count: number; }; // Sanitize 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, ""); return clean; } // --- Index/overview queries --- export async function getLetterYears(): Promise { const letters = await prisma.letter.findMany({ where: { year: { not: null } }, select: { year: true }, }); const counts: Record = {}; for (const l of letters) { if (l.year) { counts[l.year] = (counts[l.year] || 0) + 1; } } return Object.entries(counts) .map(([year, count]) => ({ year: Number(year), count })) .filter((g) => g.year >= 1940 && g.year <= 1980) .sort((a, b) => a.year - b.year); } /** * Get all unique individual recipients with letter counts. * * Each letter has a `recipients` array (e.g., ["Mukunda", "Janaki"]). * This function aggregates across all letters so that "Mukunda" appears * once with count = total letters where Mukunda is ANY recipient. */ export async function getLetterRecipients(): Promise { const letters = await prisma.letter.findMany({ select: { recipients: true }, }); const counts: Record = {}; for (const l of letters) { for (const r of l.recipients) { counts[r] = (counts[r] || 0) + 1; } } return Object.entries(counts) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count); } export async function getLetterLocations(): Promise { const letters = await prisma.letter.findMany({ where: { location: { not: null } }, select: { location: true }, }); const counts: Record = {}; for (const l of letters) { if (l.location) { counts[l.location] = (counts[l.location] || 0) + 1; } } return Object.entries(counts) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count); } // --- Letter list queries --- export async function getLettersByYear(year: number): Promise { const letters = await prisma.letter.findMany({ where: { year }, orderBy: [{ month: "asc" }, { day: "asc" }], select: { slug: true, recipients: true, recipientDisplay: true, date: true, year: true, month: true, location: true, }, }); return letters; } /** * Get all letters where the given name is ANY recipient. * * Uses Prisma's `has` filter on the recipients array. * Searching for "Mukunda" returns letters addressed to: * - "Mukunda" (individual) * - "Mukunda, Janaki" (group containing Mukunda) * - "Yamuna, Mukunda, Syamasundara" (group containing Mukunda) */ export async function getLettersByRecipient( recipient: string, ): Promise { const letters = await prisma.letter.findMany({ where: { recipients: { has: recipient } }, orderBy: [{ year: "asc" }, { month: "asc" }, { day: "asc" }], select: { slug: true, recipients: true, recipientDisplay: true, date: true, year: true, month: true, location: true, }, }); return letters; } export async function getLettersByLocation( location: string, ): Promise { const letters = await prisma.letter.findMany({ where: { location }, orderBy: [{ year: "asc" }, { month: "asc" }, { day: "asc" }], select: { slug: true, recipients: true, recipientDisplay: true, date: true, year: true, month: true, location: true, }, }); return letters; } // --- Single letter --- export async function getLetter(slug: string): Promise { const letter = await prisma.letter.findUnique({ where: { slug } }); if (!letter) return null; return { slug: letter.slug, recipients: letter.recipients, recipientDisplay: letter.recipientDisplay, date: letter.date, dateRef: letter.dateRef, year: letter.year, month: letter.month, day: letter.day, location: letter.location, salutation: letter.salutation, paragraphs: letter.paragraphs.map(sanitizeHtml), }; } // --- Navigation helpers --- export async function getAdjacentLetters( slug: string, year: number | null, ): Promise<{ prev: LetterSummary | null; next: LetterSummary | null }> { if (!year) return { prev: null, next: null }; const yearLetters = await prisma.letter.findMany({ where: { year }, orderBy: [{ month: "asc" }, { day: "asc" }, { slug: "asc" }], select: { slug: true, recipients: true, recipientDisplay: true, date: true, year: true, month: true, location: true, }, }); const idx = yearLetters.findIndex((l) => l.slug === slug); return { prev: idx > 0 ? yearLetters[idx - 1] : null, next: idx < yearLetters.length - 1 ? yearLetters[idx + 1] : null, }; } export async function getTotalLetterCount(): Promise { return prisma.letter.count(); }