318 lines
8.2 KiB
TypeScript
318 lines
8.2 KiB
TypeScript
/**
|
|
* 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<number> {
|
|
return prisma.disciple.count();
|
|
}
|
|
|
|
/** Get count of disciples matching optional letter + suffix filters. */
|
|
export async function getFilteredDiscipleCount(
|
|
letter: string | null,
|
|
suffix: SuffixFilter = "all",
|
|
): Promise<number> {
|
|
const where: Record<string, unknown> = {};
|
|
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<DiscipleSummary[]> {
|
|
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<DiscipleSummary[]> {
|
|
const where: Record<string, unknown> = {};
|
|
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<DiscipleSummary[]> {
|
|
const where: Record<string, unknown> = {};
|
|
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<string, number> = {};
|
|
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<LetterCount[]> {
|
|
const disciples = await prisma.disciple.findMany({
|
|
select: { initiatedName: true },
|
|
});
|
|
|
|
const counts: Record<string, number> = {};
|
|
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<PlaceGroup[]> {
|
|
const disciples = await prisma.disciple.findMany({
|
|
where: { initiationPlace: { not: null } },
|
|
select: { initiationPlace: true },
|
|
});
|
|
|
|
const counts: Record<string, number> = {};
|
|
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<DiscipleSummary[]> {
|
|
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<YearGroup[]> {
|
|
const disciples = await prisma.disciple.findMany({
|
|
where: { initiationDate: { not: null } },
|
|
select: { initiationDate: true },
|
|
});
|
|
|
|
const counts: Record<number, number> = {};
|
|
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<DiscipleSummary[]> {
|
|
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<DiscipleSummary[]> {
|
|
const disciples = await prisma.disciple.findMany({
|
|
where: {
|
|
initiatedName: {
|
|
contains: query,
|
|
mode: "insensitive",
|
|
},
|
|
},
|
|
orderBy: { initiatedName: "asc" },
|
|
take: 100,
|
|
select: discipleSummarySelect,
|
|
});
|
|
return disciples;
|
|
}
|