feat: implement bff layer

This commit is contained in:
gosangam
2026-05-10 00:35:14 +05:30
parent 5951eb5e18
commit 32da38b24f
91 changed files with 2434 additions and 11784 deletions
+87 -55
View File
@@ -1,18 +1,14 @@
/**
* Data access layer for Other Documents.
*
* Uses dedicated Prisma models:
* - OtherDocumentMeta (collection: other_document_meta)
* - OtherDocumentChapter (collection: other_document_chapters)
*
* Calls the BFF API for all data access (no direct Prisma usage).
* Falls back to reading local JSON files from
* lib/jsons/other-documents/<slug>/ when the database is unavailable.
* lib/jsons/other-documents/<slug>/ when the BFF is unavailable.
*
* Book codes are plain slugs (e.g. "analysis-bg", "real-peace-formula").
*/
import { prisma } from "@/lib/db";
import type { JsonValue } from "@prisma/client/runtime/library";
import { bff, bffList } from "@/lib/bff";
import fs from "fs";
import path from "path";
@@ -72,6 +68,33 @@ export type OtherDocumentSidebarItem = {
chapter: number;
};
// ---------------------------------------------------------------------------
// BFF response types (snake_case from Go)
// ---------------------------------------------------------------------------
type BffDocumentMeta = {
id: string;
book: string;
data: {
title: string;
author: string;
year?: number | null;
description?: string | null;
totalChapters: number;
chapters: { number: number; title: string }[];
};
};
type BffChapter = {
id: string;
book: string;
chapter: number;
title: string;
type: string;
slug: string;
paragraphs: string[];
};
// ---------------------------------------------------------------------------
// HTML sanitisation (same pattern used across all readers)
// ---------------------------------------------------------------------------
@@ -90,23 +113,52 @@ function sanitizeHtml(html: string): string {
return clean;
}
function normalizeParagraphs(paragraphs: string[] | JsonValue): string[] {
let result: string[];
// ---------------------------------------------------------------------------
// Mappers (BFF response → frontend types)
// ---------------------------------------------------------------------------
if (typeof paragraphs === "string") {
try {
const parsed = JSON.parse(paragraphs);
result = Array.isArray(parsed) ? parsed : [paragraphs];
} catch {
result = [paragraphs];
}
} else if (Array.isArray(paragraphs)) {
result = paragraphs as string[];
} else {
result = [];
}
function mapMetaData(raw: BffDocumentMeta): OtherDocumentMetaData {
const data =
raw.data && typeof raw.data === "object"
? raw.data
: ({} as BffDocumentMeta["data"]);
return {
book: raw.book,
title: data.title ?? "",
author: data.author ?? "",
year: data.year ?? null,
description: data.description ?? null,
totalChapters: data.totalChapters ?? 0,
chapters: (data.chapters ?? []).map((ch) => ({
number: ch.number,
title: ch.title,
})),
};
}
return result.map(sanitizeHtml);
function mapSummary(raw: BffDocumentMeta): OtherDocumentSummary {
const data =
raw.data && typeof raw.data === "object"
? raw.data
: ({} as BffDocumentMeta["data"]);
return {
book: raw.book,
title: data.title ?? "",
author: data.author ?? "",
year: data.year ?? null,
description: data.description ?? null,
totalChapters: data.totalChapters ?? 0,
};
}
function mapChapter(raw: BffChapter): OtherDocumentChapterData {
return {
book: raw.book,
chapter: raw.chapter,
title: raw.title,
type: raw.type,
paragraphs: (raw.paragraphs ?? []).map(sanitizeHtml),
};
}
// ---------------------------------------------------------------------------
@@ -195,22 +247,12 @@ function getLocalChapter(
*/
export async function getAllOtherDocuments(): Promise<OtherDocumentSummary[]> {
try {
const results = await prisma.otherDocumentMeta.findMany({
orderBy: { book: "asc" },
const results = await bffList<BffDocumentMeta>("/other-documents", {
tags: ["other-documents"],
});
if (results.length > 0) {
return results.map((row) => {
const data = row.data as unknown as OtherDocumentMetaData;
return {
book: row.book,
title: data.title,
author: data.author,
year: data.year ?? null,
description: data.description ?? null,
totalChapters: data.totalChapters,
};
});
return results.map(mapSummary);
}
} catch {
// fall through to local JSON
@@ -234,12 +276,12 @@ export async function getOtherDocumentMeta(
bookCode: string,
): Promise<OtherDocumentMetaData | null> {
try {
const row = await prisma.otherDocumentMeta.findUnique({
where: { book: bookCode },
const raw = await bff<BffDocumentMeta>(`/other-documents/${bookCode}`, {
tags: [`other-doc-${bookCode}`],
});
if (row) {
return row.data as unknown as OtherDocumentMetaData;
if (raw) {
return mapMetaData(raw);
}
} catch {
// fall through to local JSON
@@ -258,23 +300,13 @@ export async function getOtherDocumentChapter(
if (chapterNum < 1 || !Number.isInteger(chapterNum)) return null;
try {
const doc = await prisma.otherDocumentChapter.findUnique({
where: {
book_slug: {
book: bookCode,
slug: String(chapterNum),
},
},
});
const raw = await bff<BffChapter>(
`/other-documents/${bookCode}/chapters/${chapterNum}`,
{ tags: [`other-doc-${bookCode}`] },
);
if (doc) {
return {
book: doc.book,
chapter: doc.chapter,
title: doc.title,
type: doc.type,
paragraphs: normalizeParagraphs(doc.paragraphs),
};
if (raw) {
return mapChapter(raw);
}
} catch {
// fall through to local JSON