203 lines
5.6 KiB
TypeScript
203 lines
5.6 KiB
TypeScript
/**
|
||
* 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: <intro.json> }
|
||
* - 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<T>(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<ReturnType<MongoClient["db"]>["collection"]>,
|
||
docs: Record<string, unknown>[],
|
||
) {
|
||
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<MongoClient["db"]>) {
|
||
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<LclIntroJson>(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<string, unknown>[] = [];
|
||
|
||
// 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<LclIntroSegmentJson>(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<LclChapterJson>(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);
|
||
});
|