/** * seed-lcl.ts — Push Life Comes from Life JSON data into MongoDB. * * Uses the native MongoDB driver (same pattern as seed-pqpa.ts) to avoid * Prisma replica-set write requirements. * * LCL is a prose/dialogue book (morning walk conversations) with no volume structure. * We use volume=0 as the sentinel for "no volumes". * * Collections written: * - book_meta → one doc { book: "lcl", data: } * - prose_chapters → intro segments + 16 chapters * * Usage: * npx tsx scripts/seed-lcl.ts */ import dotenv from "dotenv"; import fs from "fs"; import path from "path"; import { MongoClient } from "mongodb"; // Load env dotenv.config({ path: ".env.local" }); dotenv.config({ path: ".env" }); const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.error("❌ DATABASE_URL is not set in .env"); process.exit(1); } const BASE_DIR = path.resolve(__dirname, ".."); const LCL_DIR = path.join(BASE_DIR, "lib", "jsons", "books", "lcl"); /** Volume sentinel for books without volumes. */ const NO_VOLUME = 0; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function readJson(filePath: string): T { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } /** * Insert documents one at a time with a small delay every N docs. * Uses insertOne to avoid BSON wire-protocol corruption. */ async function insertBatched( col: ReturnType["collection"]>, docs: Record[], ) { for (let i = 0; i < docs.length; i++) { await col.insertOne(docs[i]); if ((i + 1) % 100 === 0) { await new Promise((r) => setTimeout(r, 100)); } } } // --------------------------------------------------------------------------- // Types for the JSON files // --------------------------------------------------------------------------- type LclIntroJson = { book: string; title: string; author: string; year: number; description: string; totalChapters: number; chapters: { number: number; title: string }[]; }; type LclIntroSegmentJson = { slug: string; title: string; type: string; book: string; order: number; paragraphs: string[]; }; type LclChapterJson = { book: string; chapter: number | null; title: string; type: string; paragraphs: string[]; }; // --------------------------------------------------------------------------- // Seed function // --------------------------------------------------------------------------- async function seedLcl(db: ReturnType) { const introPath = path.join(LCL_DIR, "intro.json"); if (!fs.existsSync(introPath)) { console.error("❌ LCL intro.json not found at", introPath); process.exit(1); } const intro = readJson(introPath); // ---- Book meta ---- const metaCol = db.collection("book_meta"); await metaCol.deleteMany({ book: "lcl" }); await metaCol.insertOne({ book: "lcl", data: intro }); console.log(" 📖 LCL book meta inserted"); // ---- Prose chapters collection ---- const proseCol = db.collection("prose_chapters"); await proseCol.deleteMany({ book: "lcl" }); const docs: Record[] = []; // Intro segments (foreword, introduction, about-the-author) const introDir = path.join(LCL_DIR, "intro"); if (fs.existsSync(introDir)) { const introFiles = fs .readdirSync(introDir) .filter((f) => f.endsWith(".json")) .sort(); for (const file of introFiles) { const seg = readJson(path.join(introDir, file)); docs.push({ book: "lcl", volume: NO_VOLUME, chapter: null, title: seg.title, type: seg.type || "intro-segment", slug: `intro-${seg.slug || file.replace(".json", "")}`, order: seg.order ?? 0, paragraphs: seg.paragraphs, }); } console.log(` 📄 Intro segments: ${introFiles.length}`); } // Chapters (01.json – 16.json) const chaptersDir = path.join(LCL_DIR, "chapters"); if (fs.existsSync(chaptersDir)) { const chapterFiles = fs .readdirSync(chaptersDir) .filter((f) => f.endsWith(".json")) .sort(); for (const file of chapterFiles) { const ch = readJson(path.join(chaptersDir, file)); const chNum = ch.chapter ?? parseInt(file.replace(".json", ""), 10); docs.push({ book: "lcl", volume: NO_VOLUME, chapter: chNum, title: ch.title, type: ch.type || "chapter", slug: String(chNum), order: 100 + chNum, paragraphs: ch.paragraphs, }); } console.log(` 📖 Chapters: ${chapterFiles.length}`); } // Insert all docs console.log(`\n Inserting ${docs.length} documents...`); await insertBatched(proseCol, docs); const totalDocs = await proseCol.countDocuments({ book: "lcl" }); console.log(` ✅ LCL: ${totalDocs} prose entries in MongoDB`); } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main() { console.log("🌱 Seeding Life Comes from Life...\n"); const client = new MongoClient(DATABASE_URL!); await client.connect(); console.log("📡 Connected to MongoDB\n"); const db = client.db(); const start = Date.now(); await seedLcl(db); const elapsed = ((Date.now() - start) / 1000).toFixed(1); console.log(`\n🎉 Done in ${elapsed}s`); await client.close(); } main().catch((err) => { console.error("❌ Seed failed:", err); process.exit(1); });