/** * Data access layer for Disciples. * * Provides queries for browsing disciples by letter, place, year, and search. * Uses the Prisma Disciple model (collection: disciples). * * Disciples have an initiatedName and suffix (e.g., "Dasa", "Devi Dasi", "Sw"). * The letter index aggregates disciples by the first letter of their initiatedName. * Place and year groupings allow browsing by initiation location and year. */ import { prisma } from "@/lib/db"; // Types export type DiscipleSummary = { id: string; initiatedName: string; suffix: string; initiationDate: Date | null; initiationPlace: string | null; isVerified: boolean; }; export type LetterCount = { letter: string; count: number; }; export type PlaceGroup = { name: string; count: number; }; export type YearGroup = { year: number; count: number; }; // --- Summary select clause (reused across queries) --- const discipleSummarySelect = { id: true, initiatedName: true, suffix: true, initiationDate: true, initiationPlace: true, isVerified: true, } as const; // --- Suffix filter type --- export type SuffixFilter = "all" | "Dasa" | "Devi Dasi"; // --- Count --- /** Get total number of disciples in the collection. */ export async function getTotalDiscipleCount(): Promise { return prisma.disciple.count(); } /** Get count of disciples matching optional letter + suffix filters. */ export async function getFilteredDiscipleCount( letter: string | null, suffix: SuffixFilter = "all", ): Promise { const where: Record = {}; if (letter) { where.initiatedName = { startsWith: letter, mode: "insensitive" }; } if (suffix !== "all") { where.suffix = suffix; } return prisma.disciple.count({ where }); } // --- Letter-based queries --- /** * Get all disciples whose initiatedName starts with the given letter. * * Uses Prisma's `startsWith` filter with case-insensitive mode. * Results are ordered by initiatedName ascending. */ export async function getDisciplesByLetter( letter: string, ): Promise { const disciples = await prisma.disciple.findMany({ where: { initiatedName: { startsWith: letter, mode: "insensitive", }, }, orderBy: { initiatedName: "asc" }, select: discipleSummarySelect, }); return disciples; } /** * Get all disciples, optionally filtered by suffix. * * Returns all 4,700+ disciples when no suffix filter is applied. * Ordered by initiatedName ascending. */ export async function getAllDisciples( suffix: SuffixFilter = "all", ): Promise { const where: Record = {}; if (suffix !== "all") { where.suffix = suffix; } const disciples = await prisma.disciple.findMany({ where, orderBy: { initiatedName: "asc" }, select: discipleSummarySelect, }); return disciples; } /** * Get disciples filtered by letter and/or suffix. * * Supports: * - letter=null + suffix → all disciples with that suffix * - letter + suffix="all" → all disciples starting with that letter * - letter + suffix → disciples starting with that letter AND matching suffix * * Ordered by initiatedName ascending. */ export async function getFilteredDisciples( letter: string | null, suffix: SuffixFilter = "all", ): Promise { const where: Record = {}; if (letter) { where.initiatedName = { startsWith: letter, mode: "insensitive" }; } if (suffix !== "all") { where.suffix = suffix; } const disciples = await prisma.disciple.findMany({ where, orderBy: { initiatedName: "asc" }, select: discipleSummarySelect, }); return disciples; } /** * Get counts of disciples grouped by suffix. * * Returns { suffix: string, count: number }[] sorted by count desc. */ export async function getDiscipleSuffixCounts(): Promise< { suffix: string; count: number }[] > { const disciples = await prisma.disciple.findMany({ select: { suffix: true }, }); const counts: Record = {}; for (const d of disciples) { counts[d.suffix] = (counts[d.suffix] || 0) + 1; } return Object.entries(counts) .map(([suffix, count]) => ({ suffix, count })) .sort((a, b) => b.count - a.count); } /** * Get counts of disciples grouped by the first letter of their initiatedName. * * Fetches all initiatedName values and aggregates first letters in JavaScript * (Prisma/MongoDB doesn't support substring aggregations easily). * Returns results sorted alphabetically by letter. */ export async function getDiscipleLetterCounts(): Promise { const disciples = await prisma.disciple.findMany({ select: { initiatedName: true }, }); const counts: Record = {}; for (const d of disciples) { if (d.initiatedName && d.initiatedName.length > 0) { const letter = d.initiatedName[0].toUpperCase(); counts[letter] = (counts[letter] || 0) + 1; } } return Object.entries(counts) .map(([letter, count]) => ({ letter, count })) .sort((a, b) => a.letter.localeCompare(b.letter)); } // --- Place-based queries --- /** * Get unique initiation places with disciple counts. * * Fetches all non-null initiationPlace values and aggregates in JavaScript. * Returns results sorted by count descending. */ export async function getDisciplePlaces(): Promise { const disciples = await prisma.disciple.findMany({ where: { initiationPlace: { not: null } }, select: { initiationPlace: true }, }); const counts: Record = {}; for (const d of disciples) { if (d.initiationPlace) { counts[d.initiationPlace] = (counts[d.initiationPlace] || 0) + 1; } } return Object.entries(counts) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count); } /** * Get all disciples initiated at a given place. * * Results are ordered by initiatedName ascending. */ export async function getDisciplesByPlace( place: string, ): Promise { const disciples = await prisma.disciple.findMany({ where: { initiationPlace: place }, orderBy: { initiatedName: "asc" }, select: discipleSummarySelect, }); return disciples; } // --- Year-based queries --- /** * Get unique initiation years with disciple counts. * * Fetches all non-null initiationDate values and aggregates years in JavaScript * (Prisma/MongoDB doesn't support date-part aggregations easily). * Returns results sorted by year ascending. */ export async function getDiscipleYears(): Promise { const disciples = await prisma.disciple.findMany({ where: { initiationDate: { not: null } }, select: { initiationDate: true }, }); const counts: Record = {}; for (const d of disciples) { if (d.initiationDate) { const year = d.initiationDate.getFullYear(); counts[year] = (counts[year] || 0) + 1; } } return Object.entries(counts) .map(([year, count]) => ({ year: Number(year), count })) .sort((a, b) => a.year - b.year); } /** * Get all disciples initiated in a given year. * * Uses a date range query (gte: Jan 1 of year, lt: Jan 1 of year+1) * to match all initiationDate values within the calendar year. * Results are ordered by initiatedName ascending. */ export async function getDisciplesByYear( year: number, ): Promise { const startOfYear = new Date(year, 0, 1); const startOfNextYear = new Date(year + 1, 0, 1); const disciples = await prisma.disciple.findMany({ where: { initiationDate: { gte: startOfYear, lt: startOfNextYear, }, }, orderBy: { initiatedName: "asc" }, select: discipleSummarySelect, }); return disciples; } // --- Search --- /** * Search disciples by initiatedName using case-insensitive matching. * * Uses Prisma's `contains` filter with case-insensitive mode. * Results are limited to 100 and ordered by initiatedName ascending. */ export async function searchDisciples( query: string, ): Promise { const disciples = await prisma.disciple.findMany({ where: { initiatedName: { contains: query, mode: "insensitive", }, }, orderBy: { initiatedName: "asc" }, take: 100, select: discipleSummarySelect, }); return disciples; }