feat: implement bff layer
This commit is contained in:
+114
-132
@@ -2,14 +2,14 @@
|
||||
* Data access layer for Disciples.
|
||||
*
|
||||
* Provides queries for browsing disciples by letter, place, year, and search.
|
||||
* Uses the Prisma Disciple model (collection: disciples).
|
||||
* 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 { prisma } from "@/lib/db";
|
||||
import { bff, bffList } from "@/lib/bff";
|
||||
|
||||
// Types
|
||||
|
||||
@@ -37,26 +37,51 @@ export type YearGroup = {
|
||||
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";
|
||||
|
||||
// --- 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<number> {
|
||||
return prisma.disciple.count();
|
||||
const result = await bff<{ total: number }>("/disciples/count");
|
||||
return result?.total ?? 0;
|
||||
}
|
||||
|
||||
/** Get count of disciples matching optional letter + suffix filters. */
|
||||
@@ -64,14 +89,19 @@ 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" };
|
||||
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();
|
||||
}
|
||||
if (suffix !== "all") {
|
||||
where.suffix = suffix;
|
||||
}
|
||||
return prisma.disciple.count({ where });
|
||||
|
||||
// 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 ---
|
||||
@@ -79,23 +109,16 @@ export async function getFilteredDiscipleCount(
|
||||
/**
|
||||
* 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.
|
||||
* 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 prisma.disciple.findMany({
|
||||
where: {
|
||||
initiatedName: {
|
||||
startsWith: letter,
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
orderBy: { initiatedName: "asc" },
|
||||
select: discipleSummarySelect,
|
||||
const disciples = await bffList<BffDisciple>("/disciples", {
|
||||
params: { letter },
|
||||
});
|
||||
return disciples;
|
||||
return disciples.map(mapDisciple);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,16 +130,11 @@ export async function getDisciplesByLetter(
|
||||
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;
|
||||
const params: Record<string, string> = {};
|
||||
if (suffix !== "all") params.suffix = suffix;
|
||||
|
||||
const disciples = await bffList<BffDisciple>("/disciples", { params });
|
||||
return disciples.map(mapDisciple);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,19 +151,12 @@ 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;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,42 +167,22 @@ export async function getFilteredDisciples(
|
||||
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 }))
|
||||
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.
|
||||
*
|
||||
* Fetches all initiatedName values and aggregates first letters in JavaScript
|
||||
* (Prisma/MongoDB doesn't support substring aggregations easily).
|
||||
* Uses the BFF /disciples/letters endpoint which returns pre-aggregated data.
|
||||
* 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 }))
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -200,19 +191,18 @@ export async function getDiscipleLetterCounts(): Promise<LetterCount[]> {
|
||||
/**
|
||||
* Get unique initiation places with disciple counts.
|
||||
*
|
||||
* Fetches all non-null initiationPlace values and aggregates in JavaScript.
|
||||
* 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 prisma.disciple.findMany({
|
||||
where: { initiationPlace: { not: null } },
|
||||
select: { initiationPlace: true },
|
||||
});
|
||||
const disciples = await bffList<BffDisciple>("/disciples");
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
for (const d of disciples) {
|
||||
if (d.initiationPlace) {
|
||||
counts[d.initiationPlace] = (counts[d.initiationPlace] || 0) + 1;
|
||||
if (d.initiation_place) {
|
||||
counts[d.initiation_place] = (counts[d.initiation_place] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,17 +214,20 @@ export async function getDisciplePlaces(): Promise<PlaceGroup[]> {
|
||||
/**
|
||||
* 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 prisma.disciple.findMany({
|
||||
where: { initiationPlace: place },
|
||||
orderBy: { initiatedName: "asc" },
|
||||
select: discipleSummarySelect,
|
||||
});
|
||||
return disciples;
|
||||
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 ---
|
||||
@@ -242,21 +235,21 @@ export async function getDisciplesByPlace(
|
||||
/**
|
||||
* 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).
|
||||
* 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 prisma.disciple.findMany({
|
||||
where: { initiationDate: { not: null } },
|
||||
select: { initiationDate: true },
|
||||
});
|
||||
const disciples = await bffList<BffDisciple>("/disciples");
|
||||
|
||||
const counts: Record<number, number> = {};
|
||||
for (const d of disciples) {
|
||||
if (d.initiationDate) {
|
||||
const year = d.initiationDate.getFullYear();
|
||||
counts[year] = (counts[year] || 0) + 1;
|
||||
if (d.initiation_date) {
|
||||
const year = new Date(d.initiation_date).getFullYear();
|
||||
if (!isNaN(year)) {
|
||||
counts[year] = (counts[year] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,27 +261,24 @@ export async function getDiscipleYears(): Promise<YearGroup[]> {
|
||||
/**
|
||||
* 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.
|
||||
* 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 startOfYear = new Date(year, 0, 1);
|
||||
const startOfNextYear = new Date(year + 1, 0, 1);
|
||||
const disciples = await bffList<BffDisciple>("/disciples");
|
||||
|
||||
const disciples = await prisma.disciple.findMany({
|
||||
where: {
|
||||
initiationDate: {
|
||||
gte: startOfYear,
|
||||
lt: startOfNextYear,
|
||||
},
|
||||
},
|
||||
orderBy: { initiatedName: "asc" },
|
||||
select: discipleSummarySelect,
|
||||
});
|
||||
return 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 ---
|
||||
@@ -296,22 +286,14 @@ export async function getDisciplesByYear(
|
||||
/**
|
||||
* Search disciples by initiatedName using case-insensitive matching.
|
||||
*
|
||||
* Uses Prisma's `contains` filter with case-insensitive mode.
|
||||
* 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 prisma.disciple.findMany({
|
||||
where: {
|
||||
initiatedName: {
|
||||
contains: query,
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
orderBy: { initiatedName: "asc" },
|
||||
take: 100,
|
||||
select: discipleSummarySelect,
|
||||
const disciples = await bffList<BffDisciple>("/disciples/search", {
|
||||
params: { q: query },
|
||||
});
|
||||
return disciples;
|
||||
return disciples.map(mapDisciple);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user