172 lines
4.8 KiB
TypeScript
172 lines
4.8 KiB
TypeScript
/**
|
|
* seed-journals.ts — Push Journals into MongoDB.
|
|
*
|
|
* Collections written:
|
|
* - journal_meta → one doc per journal
|
|
* - journal_chapters → N chapter docs per journal
|
|
*
|
|
* Usage:
|
|
* npx tsx scripts/seed-journals.ts
|
|
*/
|
|
|
|
import dotenv from "dotenv";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { MongoClient } from "mongodb";
|
|
|
|
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");
|
|
process.exit(1);
|
|
}
|
|
|
|
const BASE_DIR = path.resolve(__dirname, "..");
|
|
const JOURNALS_DIR = path.join(BASE_DIR, "lib", "jsons", "journals");
|
|
|
|
type IntroJson = {
|
|
book: string;
|
|
title: string;
|
|
author: string;
|
|
year: number | null;
|
|
description: string | null;
|
|
totalChapters: number;
|
|
chapters: unknown[];
|
|
};
|
|
type ChapterJson = {
|
|
book: string;
|
|
chapter: number;
|
|
title: string;
|
|
type: string;
|
|
slug?: string;
|
|
paragraphs: string[];
|
|
};
|
|
|
|
function readJson<T>(filePath: string): T {
|
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
}
|
|
|
|
function sanitizeDoc(obj: unknown): unknown {
|
|
if (typeof obj === "string") {
|
|
let s = obj.normalize("NFC");
|
|
s = s.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
|
|
s = s.replace(/[\uD800-\uDFFF]/g, "");
|
|
s = s.replace(/[\u200B-\u200F\u2028-\u202F\uFEFF]/g, "");
|
|
s = Buffer.from(s, "utf-8").toString("utf-8");
|
|
return s;
|
|
}
|
|
if (Array.isArray(obj)) return obj.map(sanitizeDoc);
|
|
if (obj !== null && typeof obj === "object") {
|
|
const result: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>))
|
|
result[key] = sanitizeDoc(value);
|
|
return result;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
async function seedOneJournal(
|
|
db: ReturnType<MongoClient["db"]>,
|
|
docDir: string,
|
|
): Promise<{ bookCode: string; chapters: number }> {
|
|
const introPath = path.join(docDir, "intro.json");
|
|
if (!fs.existsSync(introPath)) throw new Error(`No intro.json in ${docDir}`);
|
|
const intro = sanitizeDoc(readJson<IntroJson>(introPath)) as IntroJson;
|
|
const bookCode = intro.book;
|
|
|
|
const metaCol = db.collection("journal_meta");
|
|
await metaCol.deleteMany({ book: bookCode });
|
|
await metaCol.insertOne({ book: bookCode, data: intro });
|
|
console.log(` 📖 Inserted journal_meta for "${intro.title}" (${bookCode})`);
|
|
|
|
const chaptersCol = db.collection("journal_chapters");
|
|
await chaptersCol.deleteMany({ book: bookCode });
|
|
|
|
const chaptersDir = path.join(docDir, "chapters");
|
|
let inserted = 0;
|
|
if (fs.existsSync(chaptersDir)) {
|
|
const files = fs
|
|
.readdirSync(chaptersDir)
|
|
.filter((f) => f.endsWith(".json"))
|
|
.sort();
|
|
for (const file of files) {
|
|
const ch = sanitizeDoc(
|
|
readJson<ChapterJson>(path.join(chaptersDir, file)),
|
|
) as ChapterJson;
|
|
const chNum = ch.chapter ?? parseInt(file.replace(".json", ""), 10);
|
|
const slug = ch.slug ?? String(chNum);
|
|
await chaptersCol.insertOne({
|
|
book: bookCode,
|
|
chapter: chNum,
|
|
title: ch.title,
|
|
type: ch.type || "chapter",
|
|
slug,
|
|
order: 100 + chNum,
|
|
paragraphs: ch.paragraphs,
|
|
});
|
|
inserted++;
|
|
}
|
|
console.log(` 📄 Chapters: ${inserted}`);
|
|
}
|
|
|
|
const total = await chaptersCol.countDocuments({ book: bookCode });
|
|
console.log(` ✅ ${bookCode}: ${total} chapter entries`);
|
|
return { bookCode, chapters: inserted };
|
|
}
|
|
|
|
async function main() {
|
|
console.log("🌱 Seeding Journals...\n");
|
|
const client = new MongoClient(DATABASE_URL!);
|
|
await client.connect();
|
|
console.log("📡 Connected to MongoDB\n");
|
|
const db = client.db();
|
|
const start = Date.now();
|
|
|
|
if (!fs.existsSync(JOURNALS_DIR)) {
|
|
console.error(`❌ ${JOURNALS_DIR} not found`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const subdirs = fs
|
|
.readdirSync(JOURNALS_DIR)
|
|
.filter((e) => {
|
|
const p = path.join(JOURNALS_DIR, e);
|
|
return (
|
|
fs.statSync(p).isDirectory() &&
|
|
fs.existsSync(path.join(p, "intro.json"))
|
|
);
|
|
})
|
|
.sort();
|
|
|
|
if (subdirs.length === 0) {
|
|
console.error("❌ No journal directories found");
|
|
process.exit(1);
|
|
}
|
|
console.log(`📂 Found ${subdirs.length} journal(s)\n`);
|
|
|
|
let totalChapters = 0;
|
|
const bookCodes: string[] = [];
|
|
for (const subdir of subdirs) {
|
|
console.log(`\n📝 Processing: ${subdir}`);
|
|
const result = await seedOneJournal(db, path.join(JOURNALS_DIR, subdir));
|
|
bookCodes.push(result.bookCode);
|
|
totalChapters += result.chapters;
|
|
}
|
|
|
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
console.log("\n" + "─".repeat(50));
|
|
console.log(
|
|
`📊 Summary: ${subdirs.length} journals, ${totalChapters} chapters`,
|
|
);
|
|
console.log(` Book codes: ${bookCodes.join(", ")}`);
|
|
console.log(`\n🎉 Done in ${elapsed}s`);
|
|
await client.close();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("❌ Seed failed:", err);
|
|
process.exit(1);
|
|
});
|