feat: implement bff layer
This commit is contained in:
@@ -1,167 +1,29 @@
|
||||
/**
|
||||
* Data access layer for Vaiśiṣṭyāṣṭakam (VA).
|
||||
*
|
||||
* Reads from MongoDB via Prisma.
|
||||
* VA is a poetic work with 8 chapters (Vaiśiṣṭyas) and no intro segments.
|
||||
*
|
||||
* Collections used:
|
||||
* - book_meta (via Prisma BookMeta model)
|
||||
* - prose_chapters (via Prisma ProseChapter model)
|
||||
*/
|
||||
import {
|
||||
getBookMeta,
|
||||
getChapter,
|
||||
getContent,
|
||||
getSidebarItems,
|
||||
type BookMeta,
|
||||
type Chapter,
|
||||
type SidebarItem,
|
||||
} from "@/lib/books";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import type { JsonValue } from "@prisma/client/runtime/library";
|
||||
const CODE = "va";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
export type VaMeta = BookMeta;
|
||||
export type VaChapterEntry = { number: number; title: string };
|
||||
export type VaChapter = Chapter;
|
||||
export type VaSidebarItem = SidebarItem;
|
||||
|
||||
const BOOK = "va";
|
||||
|
||||
/** Volume sentinel for books without volumes. */
|
||||
const NO_VOLUME = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type VaMeta = {
|
||||
book: string;
|
||||
title: string;
|
||||
author: string;
|
||||
year: number;
|
||||
description: string;
|
||||
totalChapters: number;
|
||||
chapters: VaChapterEntry[];
|
||||
};
|
||||
|
||||
export type VaChapterEntry = {
|
||||
number: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type VaChapter = {
|
||||
book: string;
|
||||
chapter: number | null;
|
||||
title: string;
|
||||
type: string;
|
||||
slug?: string;
|
||||
paragraphs: string[];
|
||||
};
|
||||
|
||||
/** Used in sidebar / table-of-contents navigation. */
|
||||
export type VaSidebarItem = {
|
||||
slug: string;
|
||||
label: string;
|
||||
type: string;
|
||||
chapter: number | null;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTML sanitisation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sanitizeHtml(html: string): string {
|
||||
let clean = html.replace(/<script[\s>][\s\S]*?<\/script>/gi, "");
|
||||
clean = clean.replace(/<style[\s>][\s\S]*?<\/style>/gi, "");
|
||||
clean = clean.replace(/<iframe[\s>][\s\S]*?<\/iframe>/gi, "");
|
||||
clean = clean.replace(
|
||||
/<\/?(?:script|style|iframe|object|embed|form|input|textarea|select|button)[^>]*>/gi,
|
||||
"",
|
||||
);
|
||||
clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
|
||||
clean = clean.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href=""');
|
||||
clean = clean.replace(/href\s*=\s*'javascript:[^']*'/gi, "href=''");
|
||||
return clean;
|
||||
export async function getVaMeta() {
|
||||
return getBookMeta(CODE);
|
||||
}
|
||||
|
||||
function normalizeParagraphs(paragraphs: string[] | JsonValue): string[] {
|
||||
let result: string[];
|
||||
|
||||
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 = [];
|
||||
}
|
||||
|
||||
return result.map(sanitizeHtml);
|
||||
export async function getVaChapter(chapterNum: number) {
|
||||
return getChapter(CODE, String(chapterNum));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Book meta
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getVaMeta(): Promise<VaMeta | null> {
|
||||
const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } });
|
||||
if (!meta) return null;
|
||||
return meta.data as unknown as VaMeta;
|
||||
export async function getVaContent(slug: string) {
|
||||
return getContent(CODE, slug);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chapters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getVaChapter(
|
||||
chapterNum: number,
|
||||
): Promise<VaChapter | null> {
|
||||
if (chapterNum < 1 || chapterNum > 8 || !Number.isInteger(chapterNum)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const doc = await prisma.proseChapter.findUnique({
|
||||
where: {
|
||||
book_volume_slug: {
|
||||
book: BOOK,
|
||||
volume: NO_VOLUME,
|
||||
slug: String(chapterNum),
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!doc) return null;
|
||||
|
||||
return {
|
||||
book: doc.book,
|
||||
chapter: doc.chapter,
|
||||
title: doc.title,
|
||||
type: doc.type,
|
||||
slug: doc.slug,
|
||||
paragraphs: normalizeParagraphs(doc.paragraphs),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unified content accessor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getVaContent(
|
||||
slug: string,
|
||||
): Promise<VaChapter | null> {
|
||||
const num = Number(slug);
|
||||
if (!Number.isNaN(num) && Number.isInteger(num) && num >= 1 && num <= 8) {
|
||||
return getVaChapter(num);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getVaSidebarItems(): Promise<VaSidebarItem[]> {
|
||||
const meta = await getVaMeta();
|
||||
if (!meta) return [];
|
||||
|
||||
return meta.chapters.map((ch) => ({
|
||||
slug: String(ch.number),
|
||||
label: `${ch.number}. ${ch.title}`,
|
||||
type: "chapter",
|
||||
chapter: ch.number,
|
||||
}));
|
||||
export async function getVaSidebarItems() {
|
||||
return getSidebarItems(CODE);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user