238 lines
6.6 KiB
TypeScript
238 lines
6.6 KiB
TypeScript
/**
|
|
* seed-ds.ts — Push Dialectic Spiritualism JSON data into MongoDB.
|
|
*
|
|
* Uses the native MongoDB driver (same pattern as seed-ssr.ts) to avoid
|
|
* Prisma replica-set write requirements.
|
|
*
|
|
* DS is a large prose/dialogue book with no volume structure.
|
|
* We use volume=0 as the sentinel for "no volumes".
|
|
*
|
|
* Sub-chapters are stored with slugs like "1-1", "1-2", "2-1", etc.
|
|
* Each sub-chapter JSON has parentChapter and subChapter fields used to
|
|
* compute a stable ordering: parentChapter * 100 + subChapter.
|
|
*
|
|
* Collections written:
|
|
* - book_meta → one doc { book: "ds", data: <intro.json> }
|
|
* - prose_chapters → 3 intro segments + 38 sub-chapters
|
|
*
|
|
* Usage:
|
|
* npx tsx scripts/seed-ds.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 DS_DIR = path.join(BASE_DIR, "lib", "jsons", "books", "ds");
|
|
|
|
/** 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 DsIntroJson = {
|
|
book: string;
|
|
title: string;
|
|
subtitle: string;
|
|
author: string;
|
|
year: number;
|
|
description: string;
|
|
totalChapters: number;
|
|
chapters: {
|
|
number: number;
|
|
title: string;
|
|
subChapters: { slug: string; title: string }[];
|
|
}[];
|
|
};
|
|
|
|
type DsIntroSegmentJson = {
|
|
slug: string;
|
|
title: string;
|
|
type: string;
|
|
book: string;
|
|
order: number;
|
|
paragraphs: string[];
|
|
};
|
|
|
|
type DsChapterJson = {
|
|
book: string;
|
|
chapter: string; // "1-1", "2-3", etc.
|
|
parentChapter: number;
|
|
subChapter: number;
|
|
title: string;
|
|
parentTitle: string;
|
|
type: string;
|
|
paragraphs: string[];
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Seed function
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function seedDs(db: ReturnType<MongoClient["db"]>) {
|
|
const introPath = path.join(DS_DIR, "intro.json");
|
|
if (!fs.existsSync(introPath)) {
|
|
console.error("❌ DS intro.json not found at", introPath);
|
|
process.exit(1);
|
|
}
|
|
|
|
const intro = readJson<DsIntroJson>(introPath);
|
|
|
|
// ---- 1. Book meta ----
|
|
const metaCol = db.collection("book_meta");
|
|
await metaCol.deleteMany({ book: "ds" });
|
|
await metaCol.insertOne({ book: "ds", data: intro });
|
|
console.log(" 📖 DS book meta inserted");
|
|
|
|
// ---- 2. Prose chapters collection ----
|
|
const proseCol = db.collection("prose_chapters");
|
|
await proseCol.deleteMany({ book: "ds" });
|
|
|
|
const docs: Record<string, unknown>[] = [];
|
|
|
|
// Intro segments (3 total: front-pages, foreword, preface)
|
|
const introSegmentFiles = [
|
|
"front-pages.json",
|
|
"foreword.json",
|
|
"preface.json",
|
|
];
|
|
|
|
const introDir = path.join(DS_DIR, "intro");
|
|
for (const file of introSegmentFiles) {
|
|
const filePath = path.join(introDir, file);
|
|
if (!fs.existsSync(filePath)) {
|
|
console.warn(` ⚠️ Intro segment not found, skipping: ${file}`);
|
|
continue;
|
|
}
|
|
|
|
const seg = readJson<DsIntroSegmentJson>(filePath);
|
|
|
|
docs.push({
|
|
book: "ds",
|
|
volume: NO_VOLUME,
|
|
chapter: null,
|
|
title: seg.title,
|
|
type: "intro-segment",
|
|
slug: `intro-${seg.slug}`,
|
|
order: seg.order,
|
|
paragraphs: seg.paragraphs,
|
|
});
|
|
}
|
|
console.log(` 📄 Intro segments: ${docs.length}`);
|
|
|
|
// Sub-chapters (1-1.json, 1-2.json, 2-1.json, etc.)
|
|
const chaptersDir = path.join(DS_DIR, "chapters");
|
|
let chapterCount = 0;
|
|
if (fs.existsSync(chaptersDir)) {
|
|
const chapterFiles = fs
|
|
.readdirSync(chaptersDir)
|
|
.filter((f) => f.endsWith(".json"))
|
|
.sort((a, b) => {
|
|
// Sort by parentChapter then subChapter numerically
|
|
const parseSlug = (name: string) => {
|
|
const slug = name.replace(".json", "");
|
|
const parts = slug.split("-");
|
|
return {
|
|
parent: parseInt(parts[0], 10),
|
|
sub: parseInt(parts[1], 10),
|
|
};
|
|
};
|
|
const aa = parseSlug(a);
|
|
const bb = parseSlug(b);
|
|
if (aa.parent !== bb.parent) return aa.parent - bb.parent;
|
|
return aa.sub - bb.sub;
|
|
});
|
|
|
|
for (const file of chapterFiles) {
|
|
const ch = readJson<DsChapterJson>(path.join(chaptersDir, file));
|
|
const order = ch.parentChapter * 100 + ch.subChapter;
|
|
|
|
docs.push({
|
|
book: "ds",
|
|
volume: NO_VOLUME,
|
|
chapter: null, // null since chapter is a string "1-1" not an int
|
|
title: ch.title,
|
|
type: "chapter",
|
|
slug: ch.chapter, // "1-1", "1-2", "2-1", etc.
|
|
order,
|
|
paragraphs: ch.paragraphs,
|
|
});
|
|
chapterCount++;
|
|
}
|
|
console.log(` 📖 Sub-chapters: ${chapterCount}`);
|
|
}
|
|
|
|
// Insert all docs
|
|
console.log(`\n Inserting ${docs.length} documents...`);
|
|
await insertBatched(proseCol, docs);
|
|
|
|
const totalDocs = await proseCol.countDocuments({ book: "ds" });
|
|
console.log(` ✅ DS: ${totalDocs} prose entries in MongoDB`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function main() {
|
|
console.log("🌱 Seeding Dialectic Spiritualism...\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 seedDs(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);
|
|
});
|