/** * Data access layer for Disciples. * * Provides queries for browsing disciples by letter, place, year, and search. * Uses the BFF API instead of direct Prisma queries. * * 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 { bff, bffList } from "@/lib/bff"; // 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; }; // --- Suffix filter type --- export type SuffixFilter = "all" | "Dasa" | "Devi Dasi"; // --- BFF response types (snake_case from Go API) --- type BffDisciple = { id: string; initiated_name: string; suffix: string; initiation_date: string | null; initiation_place: string | null; karmi_name: string | null; is_verified: boolean; }; type BffLetterCount = { _id: string; count: number; }; type BffSuffixCount = { _id: string; count: number; }; // --- Field mapping helper --- function mapDisciple(d: BffDisciple): DiscipleSummary { return { id: d.id, initiatedName: d.initiated_name, suffix: d.suffix, initiationDate: d.initiation_date ? new Date(d.initiation_date) : null, initiationPlace: d.initiation_place ?? null, isVerified: d.is_verified, }; } // --- Count --- /** Get total number of disciples in the collection. */ export async function getTotalDiscipleCount(): Promise { const result = await bff<{ total: number }>("/disciples/count"); return result?.total ?? 0; } /** Get count of disciples matching optional letter + suffix filters. */ export async function getFilteredDiscipleCount( letter: string | null, suffix: SuffixFilter = "all", ): Promise { const params: Record = {}; if (letter) params.letter = letter; if (suffix !== "all") params.suffix = suffix; // If no filters, just use total count endpoint if (!letter && suffix === "all") { return getTotalDiscipleCount(); } // Fetch filtered list and count client-side // TODO: Consider adding a dedicated BFF count endpoint with filter params for better performance const disciples = await bffList("/disciples", { params }); return disciples.length; } // --- Letter-based queries --- /** * Get all disciples whose initiatedName starts with the given letter. * * Uses BFF endpoint with letter query param. * Results are ordered by initiatedName ascending (handled by BFF). */ export async function getDisciplesByLetter( letter: string, ): Promise { const disciples = await bffList("/disciples", { params: { letter }, }); return disciples.map(mapDisciple); } /** * 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 params: Record = {}; if (suffix !== "all") params.suffix = suffix; const disciples = await bffList("/disciples", { params }); return disciples.map(mapDisciple); } /** * 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 params: Record = {}; if (letter) params.letter = letter; if (suffix !== "all") params.suffix = suffix; const disciples = await bffList("/disciples", { params }); return disciples.map(mapDisciple); } /** * 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 suffixes = await bffList("/disciples/suffixes"); return suffixes .map((s) => ({ suffix: s._id, count: s.count })) .sort((a, b) => b.count - a.count); } /** * Get counts of disciples grouped by the first letter of their initiatedName. * * Uses the BFF /disciples/letters endpoint which returns pre-aggregated data. * Returns results sorted alphabetically by letter. */ export async function getDiscipleLetterCounts(): Promise { const letters = await bffList("/disciples/letters"); return letters .map((l) => ({ letter: l._id, count: l.count })) .sort((a, b) => a.letter.localeCompare(b.letter)); } // --- Place-based queries --- /** * Get unique initiation places with disciple counts. * * No dedicated BFF endpoint — fetches all disciples and aggregates in JS. * Returns results sorted by count descending. * * TODO: Add a BFF endpoint (GET /api/v1/disciples/places) for better performance. */ export async function getDisciplePlaces(): Promise { const disciples = await bffList("/disciples"); const counts: Record = {}; for (const d of disciples) { if (d.initiation_place) { counts[d.initiation_place] = (counts[d.initiation_place] || 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. * * No dedicated BFF endpoint — fetches all disciples and filters in JS. * Results are ordered by initiatedName ascending. * * TODO: Add BFF query param support (GET /api/v1/disciples?place=...) for better performance. */ export async function getDisciplesByPlace( place: string, ): Promise { const disciples = await bffList("/disciples"); return disciples .filter((d) => d.initiation_place === place) .map(mapDisciple) .sort((a, b) => a.initiatedName.localeCompare(b.initiatedName)); } // --- Year-based queries --- /** * Get unique initiation years with disciple counts. * * No dedicated BFF endpoint — fetches all disciples and aggregates years in JS. * Returns results sorted by year ascending. * * TODO: Add a BFF endpoint (GET /api/v1/disciples/years) for better performance. */ export async function getDiscipleYears(): Promise { const disciples = await bffList("/disciples"); const counts: Record = {}; for (const d of disciples) { if (d.initiation_date) { const year = new Date(d.initiation_date).getFullYear(); if (!isNaN(year)) { 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. * * No dedicated BFF endpoint — fetches all disciples and filters in JS. * Results are ordered by initiatedName ascending. * * TODO: Add BFF query param support (GET /api/v1/disciples?year=...) for better performance. */ export async function getDisciplesByYear( year: number, ): Promise { const disciples = await bffList("/disciples"); return disciples .filter((d) => { if (!d.initiation_date) return false; const dYear = new Date(d.initiation_date).getFullYear(); return dYear === year; }) .map(mapDisciple) .sort((a, b) => a.initiatedName.localeCompare(b.initiatedName)); } // --- Search --- /** * Search disciples by initiatedName using case-insensitive matching. * * Uses BFF search endpoint. * Results are limited to 100 and ordered by initiatedName ascending. */ export async function searchDisciples( query: string, ): Promise { const disciples = await bffList("/disciples/search", { params: { q: query }, }); return disciples.map(mapDisciple); }