From 89106efc9803f8cf1011926206605be261802cbd Mon Sep 17 00:00:00 2001 From: gosangam Date: Sun, 3 May 2026 14:46:35 +0530 Subject: [PATCH] fix: sva now extracts and reads properly --- lib/sva.ts | 240 +++++++++++++++++++------------------------- scripts/seed-sva.ts | 18 ---- 2 files changed, 106 insertions(+), 152 deletions(-) diff --git a/lib/sva.ts b/lib/sva.ts index d582a306..bc0c10cd 100644 --- a/lib/sva.ts +++ b/lib/sva.ts @@ -1,10 +1,10 @@ /** * Data access layer for Songs of the Vaiṣṇava Ācāryas (SVA). * - * Reads from MongoDB via Prisma. - * SVA is a song book with 4 parent parts, each containing multiple songs - * (sub-chapters). Each song is stored as its own prose_chapters document - * with slugs like "1-1", "1-2", "2-1", etc. + * Reads from MongoDB via Prisma, same as the rest of the app. + * SVA is a hierarchical song book with 4 parent parts, each containing + * multiple songs (sub-chapters). Each song is stored as its own + * prose_chapters document with slugs like "1-1", "1-2", "2-1", etc. * * When the DB isn't seeded yet, falls back to reading local JSON files * from lib/jsons/books/sva/. @@ -130,8 +130,8 @@ function normalizeParagraphs(paragraphs: string[] | JsonValue): string[] { // --------------------------------------------------------------------------- function readLocalJson(filePath: string): T | null { - if (!fs.existsSync(filePath)) return null; try { + if (!fs.existsSync(filePath)) return null; return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T; } catch { return null; @@ -147,39 +147,19 @@ function getLocalIntroSegments(): SvaIntroSegment[] { for (const file of files) { const filePath = path.join(introDir, file); const seg = readLocalJson(filePath); - if (seg) results.push(seg); + if (seg) { + results.push({ + ...seg, + slug: seg.slug.startsWith("intro-") ? seg.slug : `intro-${seg.slug}`, + }); + } } - return results.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); -} - -function getLocalIntroSegment(rawSlug: string): SvaIntroSegment | null { - const filePath = path.join(SVA_JSON_DIR, "intro", `${rawSlug}.json`); - return readLocalJson(filePath); + return results.sort((a, b) => a.order - b.order); } function getLocalSubChapter(slug: string): SvaChapter | null { const filePath = path.join(SVA_JSON_DIR, "chapters", `${slug}.json`); - const raw = readLocalJson<{ - book: string; - slug: string; - parentChapter: number; - subChapter: number; - title: string; - parentTitle: string; - type: string; - paragraphs: string[]; - }>(filePath); - if (!raw) return null; - return { - book: raw.book, - chapter: raw.slug, - parentChapter: raw.parentChapter, - subChapter: raw.subChapter, - title: raw.title, - parentTitle: raw.parentTitle, - type: raw.type, - paragraphs: raw.paragraphs, - }; + return readLocalJson(filePath); } // --------------------------------------------------------------------------- @@ -194,7 +174,7 @@ export async function getSvaMeta(): Promise { const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } }); if (meta) return meta.data as unknown as SvaMeta; } catch { - // Prisma BSON decode error — fall through to local JSON + // fall through to local JSON } // Fallback to local JSON @@ -227,7 +207,7 @@ export async function getSvaIntroSegments(): Promise { })); } } catch { - // Prisma BSON decode error — fall through to local JSON + // fall through to local JSON } // Fallback to local JSON @@ -236,7 +216,7 @@ export async function getSvaIntroSegments(): Promise { /** * A single intro segment by slug. - * Accepts both "intro-front-pages" (URL slug) and "front-pages" (raw slug). + * Accepts both "intro-foreword" (URL slug) and "foreword" (raw slug). */ export async function getSvaIntroSegment( slug: string, @@ -247,25 +227,30 @@ export async function getSvaIntroSegment( ? slug.replace(/^intro-/, "") : slug; - const doc = await prisma.proseChapter.findUnique({ - where: { - book_volume_slug: { book: BOOK, volume: NO_VOLUME, slug: dbSlug }, - }, - }); + try { + const doc = await prisma.proseChapter.findUnique({ + where: { + book_volume_slug: { book: BOOK, volume: NO_VOLUME, slug: dbSlug }, + }, + }); - if (doc) { - return { - slug: doc.slug, - title: doc.title, - type: doc.type, - book: doc.book, - order: doc.order, - paragraphs: normalizeParagraphs(doc.paragraphs), - }; + if (doc) { + return { + slug: doc.slug, + title: doc.title, + type: doc.type, + book: doc.book, + order: doc.order, + paragraphs: normalizeParagraphs(doc.paragraphs), + }; + } + } catch { + // fall through to local JSON } // Fallback to local JSON - const local = getLocalIntroSegment(rawSlug); + const filePath = path.join(SVA_JSON_DIR, "intro", `${rawSlug}.json`); + const local = readLocalJson(filePath); if (!local) return null; return { ...local, @@ -275,7 +260,7 @@ export async function getSvaIntroSegment( } // --------------------------------------------------------------------------- -// Chapters (sub-chapters like "1-1", "2-3", etc.) +// Chapters (sub-chapters / songs like "1-1", "2-3", etc.) // --------------------------------------------------------------------------- /** @@ -318,7 +303,7 @@ export async function getSvaChapter(slug: string): Promise { }; } } catch { - // Prisma BSON decode error — fall through to local JSON + // fall through to local JSON } // Fallback to local JSON @@ -360,8 +345,10 @@ export async function getSvaContent( /** * Build the full sidebar navigation list with hierarchical structure. * - * Order: front-pages → foreword → introduction - * → parent-1 (label) → 1-1, 1-2, ... → parent-2 (label) → 2-1, 2-2, ... + * Order: intro segments (front-pages, foreword, introduction) + * → parent-1 (label) → 1-1, 1-2, ... + * → parent-2 (label) → 2-1, 2-2, ... + * → etc. * * "parent" type items are non-clickable headings. Only "intro-segment" and * "sub-chapter" items are navigable pages. @@ -378,7 +365,7 @@ export async function getSvaSidebarItems(): Promise { const meta = await getSvaMeta(); try { - allDocs = await prisma.proseChapter.findMany({ + const results = await prisma.proseChapter.findMany({ where: { book: BOOK, volume: NO_VOLUME }, orderBy: { order: "asc" }, select: { @@ -389,41 +376,36 @@ export async function getSvaSidebarItems(): Promise { order: true, }, }); + allDocs = results; } catch { - // Prisma BSON decode error — build sidebar from meta alone + // Prisma error — build sidebar from meta alone } if (!meta) { // Try building from local JSON - return buildSidebarFromLocal(); + const localMeta = readLocalJson( + path.join(SVA_JSON_DIR, "intro.json"), + ); + if (!localMeta) return []; + return buildSidebarFromMeta(localMeta); + } + + // If we have no DB docs, build from meta alone + if (allDocs.length === 0) { + return buildSidebarFromMeta(meta); } const items: SvaSidebarItem[] = []; // Intro segments that come BEFORE chapters (order < 99) - if (allDocs.length > 0) { - for (const doc of allDocs) { - if (doc.type === "intro-segment" && (doc.order ?? 0) < 99) { - items.push({ - slug: doc.slug, - label: doc.title, - type: "intro-segment", - parentChapter: null, - }); - } - } - } else { - // Fallback: local intro segments - const localIntros = getLocalIntroSegments(); - for (const seg of localIntros) { - if ((seg.order ?? 0) < 99) { - items.push({ - slug: `intro-${seg.slug}`, - label: seg.title, - type: "intro-segment", - parentChapter: null, - }); - } + for (const doc of allDocs) { + if (doc.type === "intro-segment" && doc.order < 99) { + items.push({ + slug: doc.slug, + label: doc.title, + type: "intro-segment", + parentChapter: null, + }); } } @@ -437,7 +419,7 @@ export async function getSvaSidebarItems(): Promise { parentChapter: ch.number, }); - // Sub-chapters (songs) under this parent + // Sub-chapters under this parent for (const sub of ch.subChapters) { items.push({ slug: sub.slug, @@ -449,71 +431,61 @@ export async function getSvaSidebarItems(): Promise { } // Intro segments that come AFTER chapters (order >= 99) - if (allDocs.length > 0) { - for (const doc of allDocs) { - if (doc.type === "intro-segment" && (doc.order ?? 0) >= 99) { - items.push({ - slug: doc.slug, - label: doc.title, - type: "intro-segment", - parentChapter: null, - }); - } + for (const doc of allDocs) { + if (doc.type === "intro-segment" && doc.order >= 99) { + items.push({ + slug: doc.slug, + label: doc.title, + type: "intro-segment", + parentChapter: null, + }); } } return items; } -function buildSidebarFromLocal(): SvaSidebarItem[] { +/** + * Build sidebar items purely from metadata (when DB is unavailable). + */ +function buildSidebarFromMeta(meta: SvaMeta): SvaSidebarItem[] { const items: SvaSidebarItem[] = []; - // Intro segments - const introSegs = getLocalIntroSegments(); - for (const seg of introSegs) { - if ((seg.order ?? 0) < 99) { - items.push({ - slug: `intro-${seg.slug}`, - label: seg.title, - type: "intro-segment", - parentChapter: null, - }); - } - } + // Hardcoded intro segments + items.push({ + slug: "intro-front-pages", + label: "Front Pages", + type: "intro-segment", + parentChapter: null, + }); + items.push({ + slug: "intro-foreword", + label: "Foreword", + type: "intro-segment", + parentChapter: null, + }); + items.push({ + slug: "intro-introduction", + label: "Introduction", + type: "intro-segment", + parentChapter: null, + }); - // Load meta for chapter structure - const introPath = path.join(SVA_JSON_DIR, "intro.json"); - const meta = readLocalJson(introPath); + for (const ch of meta.chapters) { + items.push({ + slug: `parent-${ch.number}`, + label: `Part ${ch.number}: ${ch.title}`, + type: "parent", + parentChapter: ch.number, + }); - if (meta?.chapters) { - for (const ch of meta.chapters) { + for (const sub of ch.subChapters) { items.push({ - slug: `parent-${ch.number}`, - label: `Part ${ch.number}: ${ch.title}`, - type: "parent", + slug: sub.slug, + label: sub.title, + type: "sub-chapter", parentChapter: ch.number, }); - - for (const sub of ch.subChapters) { - items.push({ - slug: sub.slug, - label: sub.title, - type: "sub-chapter", - parentChapter: ch.number, - }); - } - } - } - - // Back matter - for (const seg of introSegs) { - if ((seg.order ?? 0) >= 99) { - items.push({ - slug: `intro-${seg.slug}`, - label: seg.title, - type: "intro-segment", - parentChapter: null, - }); } } diff --git a/scripts/seed-sva.ts b/scripts/seed-sva.ts index 512650e0..187e3dbe 100644 --- a/scripts/seed-sva.ts +++ b/scripts/seed-sva.ts @@ -93,24 +93,6 @@ async function insertBatched( for (let i = 0; i < docs.length; i++) { const sanitized = sanitizeDoc(docs[i]) as Record; - // Work around BSON driver bug with large string arrays: - // Store paragraphs as a single JSON-encoded string instead of a native - // array. The data layer (lib/sva.ts) already handles stringified JSON - // via normalizeParagraphs(). - if (Array.isArray(sanitized.paragraphs)) { - // Stringify, then sanitize the resulting string to ensure clean UTF-8 - let jsonStr = JSON.stringify(sanitized.paragraphs); - // NFC normalize the whole JSON string - jsonStr = jsonStr.normalize("NFC"); - // Remove any stray control chars or surrogates - jsonStr = jsonStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, ""); - jsonStr = jsonStr.replace(/[\uD800-\uDFFF]/g, ""); - jsonStr = jsonStr.replace(/[\u200B-\u200F\u2028-\u202F\uFEFF]/g, ""); - // Round-trip through Buffer to guarantee valid UTF-8 byte sequence - jsonStr = Buffer.from(jsonStr, "utf-8").toString("utf-8"); - sanitized.paragraphs = jsonStr; - } - try { await col.insertOne(sanitized); } catch (err) {