349 lines
9.7 KiB
TypeScript
349 lines
9.7 KiB
TypeScript
/**
|
|
* 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";
|
|
|
|
/**
|
|
* A disciple who has a platform account, linked by an admin.
|
|
*
|
|
* The disciple records live in Mongo while accounts live in Postgres, so the
|
|
* link is resolved server-side by the BFF and handed over as this flat pair.
|
|
*/
|
|
export type DiscipleLink = {
|
|
discipleId: string;
|
|
handle: string;
|
|
name: string;
|
|
avatarURL: string | null;
|
|
};
|
|
|
|
// --- 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,
|
|
};
|
|
}
|
|
|
|
// --- Linked platform accounts ---
|
|
|
|
type BffDiscipleLink = {
|
|
disciple_id: string;
|
|
handle: string;
|
|
name: string;
|
|
avatar_url: string | null;
|
|
};
|
|
|
|
/**
|
|
* Get every disciple who has a platform account.
|
|
*
|
|
* Deliberately unpaged — only Śrīla Prabhupāda's own disciples are ever
|
|
* linked, so this is a few dozen rows at most and the browser fetches the
|
|
* whole set once to highlight the matching cards.
|
|
*
|
|
* Fails soft: the highlight is decorative, so a BFF hiccup (or a rolling
|
|
* deploy where the frontend is ahead of the API) must not take the whole
|
|
* directory page down with it. `bff` already maps 404 → null; here we also
|
|
* swallow 5xx and network errors and fall back to "no highlights".
|
|
*/
|
|
export async function getDiscipleLinks(): Promise<DiscipleLink[]> {
|
|
try {
|
|
const links = await bffList<BffDiscipleLink>("/disciples/linked");
|
|
return links.map((l) => ({
|
|
discipleId: l.disciple_id,
|
|
handle: l.handle,
|
|
name: l.name,
|
|
avatarURL: l.avatar_url ?? null,
|
|
}));
|
|
} catch (err) {
|
|
console.error("getDiscipleLinks failed; rendering without highlights", err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// --- Count ---
|
|
|
|
/** Get total number of disciples in the collection. */
|
|
export async function getTotalDiscipleCount(): Promise<number> {
|
|
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<number> {
|
|
const params: Record<string, string> = {};
|
|
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<BffDisciple>("/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<DiscipleSummary[]> {
|
|
const disciples = await bffList<BffDisciple>("/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<DiscipleSummary[]> {
|
|
const params: Record<string, string> = {};
|
|
if (suffix !== "all") params.suffix = suffix;
|
|
|
|
const disciples = await bffList<BffDisciple>("/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<DiscipleSummary[]> {
|
|
const params: Record<string, string> = {};
|
|
if (letter) params.letter = letter;
|
|
if (suffix !== "all") params.suffix = suffix;
|
|
|
|
const disciples = await bffList<BffDisciple>("/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<BffSuffixCount>("/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<LetterCount[]> {
|
|
const letters = await bffList<BffLetterCount>("/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<PlaceGroup[]> {
|
|
const disciples = await bffList<BffDisciple>("/disciples");
|
|
|
|
const counts: Record<string, number> = {};
|
|
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<DiscipleSummary[]> {
|
|
const disciples = await bffList<BffDisciple>("/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<YearGroup[]> {
|
|
const disciples = await bffList<BffDisciple>("/disciples");
|
|
|
|
const counts: Record<number, number> = {};
|
|
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<DiscipleSummary[]> {
|
|
const disciples = await bffList<BffDisciple>("/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<DiscipleSummary[]> {
|
|
const disciples = await bffList<BffDisciple>("/disciples/search", {
|
|
params: { q: query },
|
|
});
|
|
return disciples.map(mapDisciple);
|
|
}
|