Add comprehensive page management system to admin dashboard

Features added:
- Database schema for pages and media files with content types (Rich Text, HTML, Markdown)
- Admin API routes for full page CRUD operations
- Image upload functionality with file management
- Rich text editor using TinyMCE with image insertion
- Admin interface for creating/editing pages with SEO options
- Dynamic navigation and footer integration
- Public page display routes with proper SEO metadata
- Support for featured images and content excerpts

Admin features:
- Create/edit/delete pages with rich content editor
- Upload and manage images through media library
- Configure pages to appear in navigation or footer
- Set page status (Draft, Published, Archived)
- SEO title and description management
- Real-time preview of content changes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-24 07:26:25 +00:00
parent f81886a851
commit 95070e5369
53 changed files with 3628 additions and 206 deletions

View File

@@ -0,0 +1,48 @@
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
import { prisma } from '../lib/db';
async function checkAdminUser() {
try {
console.log('Checking admin user: andrei@cloudz.ro');
const user = await prisma.user.findUnique({
where: { email: 'andrei@cloudz.ro' },
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
lastLoginAt: true
}
});
if (user) {
console.log('✅ User found:', user);
if (['admin', 'moderator'].includes(user.role)) {
console.log('✅ User has admin privileges');
} else {
console.log('❌ User does not have admin role. Current role:', user.role);
console.log('Updating user role to admin...');
const updatedUser = await prisma.user.update({
where: { email: 'andrei@cloudz.ro' },
data: { role: 'admin' }
});
console.log('✅ User role updated:', updatedUser.role);
}
} else {
console.log('❌ User not found');
}
} catch (error) {
console.error('Error checking admin user:', error);
} finally {
await prisma.$disconnect();
}
}
checkAdminUser();

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const ABBR = (process.env.EN_ABBR || 'WEB').toUpperCase();
const ROOT = process.env.OUTPUT_DIR || path.join('data','en_bible', ABBR);
function cleanText(s){
return s
// remove \+w/\w wrappers and closing tags
.replace(/\\\+?w\s+/gi,'')
.replace(/\|strong="[^"]*"/gi,'')
.replace(/\\\+?w\*/gi,'')
// remove footnotes / cross-refs blocks
.replace(/\\f\s+.*?\\f\*/gis,' ')
.replace(/\\x\s+.*?\\x\*/gis,' ')
// remove +wh blocks and similar wrappers
.replace(/\\\+wh\s+.*?\\\+wh\*/gis,' ')
// remove inline verse-note blocks like "+ 1:1 ... *"
.replace(/\+\s*\d+:\d+.*?\*/g,' ')
// remove stray asterisks left after stripping tags
.replace(/\*/g,'')
// remove any other inline tags like \\qs, \\add, etc.
.replace(/\\[a-z0-9-]+\s*/gi,' ')
.replace(/\s+/g,' ').trim();
}
function processFile(file){
const p = path.join(ROOT, file);
if(!fs.existsSync(p)){
console.error('Missing', p);
process.exit(1);
}
const j = JSON.parse(fs.readFileSync(p,'utf-8'));
for(const b of j.books||[]){
for(const c of b.chapters||[]){
for(const v of c.verses||[]){
if(v.text) v.text = cleanText(String(v.text));
}
}
}
fs.writeFileSync(p, JSON.stringify(j,null,2),'utf-8');
console.log('Cleaned', p);
}
processFile('old_testament.json');
processFile('new_testament.json');

View File

@@ -0,0 +1,63 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function cleanup() {
try {
console.log('Starting cleanup of English Bible versions (keeping WEB)...')
// Ensure WEB exists
const web = await prisma.bibleVersion.findFirst({ where: { language: 'en', abbreviation: 'WEB' } })
if (!web) {
console.error('WEB version not found. Please import WEB first (via usfm-to-json + import). Aborting.')
return
}
// Gather non-WEB English versions (e.g., BSB, BSB_MD, BSB_SAMPLES, etc.)
const others = await prisma.bibleVersion.findMany({
where: { language: 'en', NOT: { abbreviation: 'WEB' } },
orderBy: { createdAt: 'asc' }
})
console.log('Found non-WEB EN versions:', others.map(v => v.abbreviation))
for (const v of others) {
console.log(`Deleting content for ${v.abbreviation} (${v.id}) ...`)
// Delete verses for all chapters under this version
const delVerses = await prisma.bibleVerse.deleteMany({
where: { chapter: { book: { versionId: v.id } } }
})
console.log(' Verses deleted:', delVerses.count)
// Delete chapters
const delCh = await prisma.bibleChapter.deleteMany({
where: { book: { versionId: v.id } }
})
console.log(' Chapters deleted:', delCh.count)
// Delete books
const delBooks = await prisma.bibleBook.deleteMany({ where: { versionId: v.id } })
console.log(' Books deleted:', delBooks.count)
// Delete version
const delVer = await prisma.bibleVersion.delete({ where: { id: v.id } })
console.log(' Version deleted:', delVer.abbreviation)
}
// Normalize defaults: set all EN isDefault=false then set WEB=true
await prisma.bibleVersion.updateMany({ where: { language: 'en' }, data: { isDefault: false } })
await prisma.bibleVersion.update({ where: { id: web.id }, data: { isDefault: true } })
console.log('Set WEB as the sole default English version.')
// Quick sanity: count WEB books
const webBooks = await prisma.bibleBook.count({ where: { versionId: web.id } })
console.log('WEB book count:', webBooks)
} catch (e) {
console.error('Cleanup failed:', e)
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
cleanup()

View File

@@ -0,0 +1,74 @@
import 'dotenv/config'
import { Pool } from 'pg'
async function main() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const schema = (process.env.VECTOR_SCHEMA || 'ai_bible').replace(/[^a-zA-Z0-9_]/g, '')
const source = `${schema}.bv_ro_fidela`
const target = `${schema}.bv_ro_cornilescu`
const client = await pool.connect()
try {
console.log('Cloning vector table from', source, 'to', target)
await client.query('BEGIN')
await client.query(`CREATE EXTENSION IF NOT EXISTS vector;`)
await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}";`)
// Create target table if not exists with same structure
await client.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = '${schema}' AND table_name = 'bv_ro_cornilescu') THEN
EXECUTE format('CREATE TABLE %I.%I (LIKE %I.%I INCLUDING ALL)', '${schema}', 'bv_ro_cornilescu', '${schema}', 'bv_ro_fidela');
END IF;
END$$;`)
// Insert rows if target empty
const cnt = await client.query(`SELECT count(*)::int AS c FROM ${target}`)
if ((cnt.rows?.[0]?.c ?? 0) === 0) {
console.log('Copying rows...')
await client.query(`
INSERT INTO ${target} (testament, book, chapter, verse, text_raw, text_norm, tsv, embedding, created_at, updated_at)
SELECT testament, book, chapter, verse, text_raw, text_norm, tsv, embedding, created_at, updated_at
FROM ${source}
ON CONFLICT DO NOTHING
`)
} else {
console.log('Target already has rows, skipping copy')
}
// Create indexes if not exist
await client.query(`
CREATE UNIQUE INDEX IF NOT EXISTS ux_ref_bv_ro_cornilescu ON ${target} (book, chapter, verse);
CREATE INDEX IF NOT EXISTS idx_tsv_bv_ro_cornilescu ON ${target} USING GIN (tsv);
CREATE INDEX IF NOT EXISTS idx_book_ch_bv_ro_cornilescu ON ${target} (book, chapter);
CREATE INDEX IF NOT EXISTS idx_testament_bv_ro_cornilescu ON ${target} (testament);
`)
await client.query('COMMIT')
console.log('Rows copied and indexes created. Running post-copy maintenance...')
// Run maintenance commands outside of transaction
await client.query(`VACUUM ANALYZE ${target};`)
try {
await client.query(`
CREATE INDEX IF NOT EXISTS idx_vec_ivfflat_bv_ro_cornilescu
ON ${target} USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
`)
} catch (e) {
console.warn('IVFFLAT index creation hit memory limits; skipping for now. You can create it later with higher maintenance_work_mem.')
}
console.log('Clone completed.')
} catch (e) {
await client.query('ROLLBACK')
console.error('Clone error:', e)
process.exit(1)
} finally {
client.release()
await pool.end()
}
}
main()

View File

@@ -0,0 +1,227 @@
import fs from 'fs'
import path from 'path'
type Verse = { verseNum: number; text: string }
type Chapter = { chapterNum: number; verses: Verse[] }
type Book = { name: string; chapters: Chapter[] }
// Mapping to determine testament + order, reused to split OT / NT
const BOOK_MAPPINGS: Record<string, { testament: 'Old Testament' | 'New Testament'; orderNum: number }> = {
// Old Testament
'Geneza': { testament: 'Old Testament', orderNum: 1 },
'Exodul': { testament: 'Old Testament', orderNum: 2 },
'Leviticul': { testament: 'Old Testament', orderNum: 3 },
'Numerii': { testament: 'Old Testament', orderNum: 4 },
'Numeri': { testament: 'Old Testament', orderNum: 4 },
'Deuteronomul': { testament: 'Old Testament', orderNum: 5 },
'Deuteronom': { testament: 'Old Testament', orderNum: 5 },
'Iosua': { testament: 'Old Testament', orderNum: 6 },
'Judecătorii': { testament: 'Old Testament', orderNum: 7 },
'Judecători': { testament: 'Old Testament', orderNum: 7 },
'Rut': { testament: 'Old Testament', orderNum: 8 },
'1 Samuel': { testament: 'Old Testament', orderNum: 9 },
'2 Samuel': { testament: 'Old Testament', orderNum: 10 },
'1 Împăraţi': { testament: 'Old Testament', orderNum: 11 },
'2 Împăraţi': { testament: 'Old Testament', orderNum: 12 },
'1 Imparati': { testament: 'Old Testament', orderNum: 11 },
'2 Imparati': { testament: 'Old Testament', orderNum: 12 },
'1 Cronici': { testament: 'Old Testament', orderNum: 13 },
'2 Cronici': { testament: 'Old Testament', orderNum: 14 },
'Ezra': { testament: 'Old Testament', orderNum: 15 },
'Neemia': { testament: 'Old Testament', orderNum: 16 },
'Estera': { testament: 'Old Testament', orderNum: 17 },
'Iov': { testament: 'Old Testament', orderNum: 18 },
'Psalmii': { testament: 'Old Testament', orderNum: 19 },
'Proverbele': { testament: 'Old Testament', orderNum: 20 },
'Proverbe': { testament: 'Old Testament', orderNum: 20 },
'Eclesiastul': { testament: 'Old Testament', orderNum: 21 },
'Cântarea Cântărilor': { testament: 'Old Testament', orderNum: 22 },
'Isaia': { testament: 'Old Testament', orderNum: 23 },
'Ieremia': { testament: 'Old Testament', orderNum: 24 },
'Plângerile': { testament: 'Old Testament', orderNum: 25 },
'Ezechiel': { testament: 'Old Testament', orderNum: 26 },
'Daniel': { testament: 'Old Testament', orderNum: 27 },
'Osea': { testament: 'Old Testament', orderNum: 28 },
'Ioel': { testament: 'Old Testament', orderNum: 29 },
'Amos': { testament: 'Old Testament', orderNum: 30 },
'Obadia': { testament: 'Old Testament', orderNum: 31 },
'Iona': { testament: 'Old Testament', orderNum: 32 },
'Mica': { testament: 'Old Testament', orderNum: 33 },
'Naum': { testament: 'Old Testament', orderNum: 34 },
'Habacuc': { testament: 'Old Testament', orderNum: 35 },
'Ţefania': { testament: 'Old Testament', orderNum: 36 },
'Țefania': { testament: 'Old Testament', orderNum: 36 },
'Hagai': { testament: 'Old Testament', orderNum: 37 },
'Zaharia': { testament: 'Old Testament', orderNum: 38 },
'Maleahi': { testament: 'Old Testament', orderNum: 39 },
// New Testament
'Matei': { testament: 'New Testament', orderNum: 40 },
'Marcu': { testament: 'New Testament', orderNum: 41 },
'Luca': { testament: 'New Testament', orderNum: 42 },
'Ioan': { testament: 'New Testament', orderNum: 43 },
'Faptele Apostolilor': { testament: 'New Testament', orderNum: 44 },
'Romani': { testament: 'New Testament', orderNum: 45 },
'1 Corinteni': { testament: 'New Testament', orderNum: 46 },
'2 Corinteni': { testament: 'New Testament', orderNum: 47 },
'Galateni': { testament: 'New Testament', orderNum: 48 },
'Efeseni': { testament: 'New Testament', orderNum: 49 },
'Filipeni': { testament: 'New Testament', orderNum: 50 },
'Coloseni': { testament: 'New Testament', orderNum: 51 },
'1 Tesaloniceni': { testament: 'New Testament', orderNum: 52 },
'2 Tesaloniceni': { testament: 'New Testament', orderNum: 53 },
'1 Timotei': { testament: 'New Testament', orderNum: 54 },
'2 Timotei': { testament: 'New Testament', orderNum: 55 },
'Tit': { testament: 'New Testament', orderNum: 56 },
'Titus': { testament: 'New Testament', orderNum: 56 },
'Filimon': { testament: 'New Testament', orderNum: 57 },
'Evrei': { testament: 'New Testament', orderNum: 58 },
'Iacov': { testament: 'New Testament', orderNum: 59 },
'1 Petru': { testament: 'New Testament', orderNum: 60 },
'2 Petru': { testament: 'New Testament', orderNum: 61 },
'1 Ioan': { testament: 'New Testament', orderNum: 62 },
'2 Ioan': { testament: 'New Testament', orderNum: 63 },
'3 Ioan': { testament: 'New Testament', orderNum: 64 },
'Iuda': { testament: 'New Testament', orderNum: 65 },
'Apocalipsa': { testament: 'New Testament', orderNum: 66 },
'Revelaţia': { testament: 'New Testament', orderNum: 66 },
'Revelația': { testament: 'New Testament', orderNum: 66 }
}
function parseRomanianBibleMarkdown(md: string): Book[] {
const lines = md.split(/\r?\n/)
const books: Book[] = []
let currentBook: Book | null = null
let currentChapter: Chapter | null = null
// The MD file uses a pattern of a centered title line with ellipses like: … GENEZA …
// We detect those as book delimiters.
const isBookHeader = (line: string) => /^…\s*.+\s*…$/.test(line.trim())
const normalizeBookName = (raw: string) => {
// Strip leading/trailing ellipsis and extra spaces
const name = raw.replace(/^…\s*|\s*…$/g, '').trim()
// Try to map known variants to our mapping keys
// The MD sometimes uses slightly different diacritics or spacing; leave as-is if not found
return name
}
for (let i = 0; i < lines.length; i++) {
const raw = lines[i].trim()
if (!raw) continue
if (isBookHeader(raw)) {
// Save previous chapter and book if they have content
if (currentChapter && currentChapter.verses.length > 0 && currentBook) {
currentBook.chapters.push(currentChapter)
}
if (currentBook && currentBook.chapters.length > 0) {
books.push(currentBook)
}
const bookName = normalizeBookName(raw)
currentBook = { name: bookName, chapters: [] }
currentChapter = null
continue
}
// Detect chapter markers like: Capitolul X (case-insensitive, diacritics tolerant)
const chapterMatch = raw.match(/^[cC][aA][pP][iI][tT][oO][lL][uU][lL]\s+(\d+)$/)
if (chapterMatch && currentBook) {
// Save previous chapter if exists
if (currentChapter && currentChapter.verses.length > 0) {
currentBook.chapters.push(currentChapter)
}
currentChapter = { chapterNum: parseInt(chapterMatch[1], 10), verses: [] }
continue
}
// Detect verses that begin with a number: "1 text..."
const verseMatch = raw.match(/^(\d+)\s+(.+)$/)
if (verseMatch && currentChapter) {
const verseNum = parseInt(verseMatch[1], 10)
let verseText = verseMatch[2].trim()
// Remove paragraph markers if present
verseText = verseText.replace(/^¶\s*/, '')
// Merge continuation lines that don't start with a verse or chapter/book markers
let j = i + 1
while (j < lines.length) {
const lookahead = lines[j].trim()
if (!lookahead) break
if (/^(\d+)\s+/.test(lookahead)) break // next verse
if (isBookHeader(lookahead)) break // next book
if (/^[cC][aA][pP][iI][tT][oO][lL][uU][lL]\s+\d+$/.test(lookahead)) break // next chapter
verseText += ' ' + lookahead
j++
}
i = j - 1
verseText = verseText.replace(/\s+/g, ' ').trim()
if (verseText) {
currentChapter.verses.push({ verseNum, text: verseText })
}
}
}
// Save last chapter and book
if (currentChapter && currentChapter.verses.length > 0 && currentBook) {
currentBook.chapters.push(currentChapter)
}
if (currentBook && currentBook.chapters.length > 0) {
books.push(currentBook)
}
return books
}
function writeTestamentJson(books: Book[], outPath: string, testament: 'Old Testament' | 'New Testament') {
const data = {
testament,
books: books.map(b => ({ name: b.name, chapters: b.chapters }))
}
fs.mkdirSync(path.dirname(outPath), { recursive: true })
fs.writeFileSync(outPath, JSON.stringify(data, null, 2), 'utf-8')
console.log(`Wrote ${outPath} with ${books.length} books.`)
}
async function main() {
const mdPath = path.join(process.cwd(), 'bibles', 'Biblia-Fidela-limba-romana.md')
if (!fs.existsSync(mdPath)) {
throw new Error(`Markdown not found at ${mdPath}`)
}
console.log(`Reading: ${mdPath}`)
const md = fs.readFileSync(mdPath, 'utf-8')
const parsedBooks = parseRomanianBibleMarkdown(md)
console.log(`Parsed ${parsedBooks.length} books from Markdown`)
// Filter and sort into OT and NT using the mapping
const otBooks: Book[] = []
const ntBooks: Book[] = []
for (const b of parsedBooks) {
const meta = BOOK_MAPPINGS[b.name]
if (!meta) {
console.warn(`Skipping unknown book in mapping: ${b.name}`)
continue
}
if (meta.testament === 'Old Testament') otBooks.push(b)
else ntBooks.push(b)
}
// Sort by canonical order
otBooks.sort((a, b) => BOOK_MAPPINGS[a.name].orderNum - BOOK_MAPPINGS[b.name].orderNum)
ntBooks.sort((a, b) => BOOK_MAPPINGS[a.name].orderNum - BOOK_MAPPINGS[b.name].orderNum)
// Write JSON outputs compatible with import-romanian-versioned.ts
const otOut = path.join(process.cwd(), 'data', 'old_testament.json')
const ntOut = path.join(process.cwd(), 'data', 'new_testament.json')
writeTestamentJson(otBooks, otOut, 'Old Testament')
writeTestamentJson(ntBooks, ntOut, 'New Testament')
}
main().catch(err => {
console.error('Conversion failed:', err)
process.exit(1)
})

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env tsx
/*
Fetch the full English Bible from api.bible with careful rate limiting.
- Requires env:
- API_BIBLE_KEY: your api.scripture.api.bible key
- API_BIBLE_BASE (optional): default https://api.scripture.api.bible
- API_BIBLE_ID (optional): specific Bible ID to fetch (overrides abbr search)
- API_BIBLE_ABBR (optional): preferred abbreviation to locate (e.g., BSB, NIV, KJV)
- OUTPUT_DIR (optional): default data/en_bible
Output: Creates OT/NT JSON in format compatible with scripts/import-romanian-versioned.ts
data/en_bible/<ABBR>/old_testament.json
data/en_bible/<ABBR>/new_testament.json
Notes on rate limits:
- Serializes requests (concurrency 1) with a base delay between calls (baseDelayMs).
- Handles 429 with Retry-After header or exponential backoff.
- Supports resume by writing per-chapter JSON; if a chapter file exists, it skips re-fetching it.
*/
import 'dotenv/config'
import fs from 'fs'
import path from 'path'
const API_KEY = process.env.API_BIBLE_KEY || ''
const API_BASE = (process.env.API_BIBLE_BASE || 'https://api.scripture.api.bible').replace(/\/$/, '')
const PREF_ABBR = process.env.API_BIBLE_ABBR || 'BSB'
const FORCE_BIBLE_ID = process.env.API_BIBLE_ID || ''
const OUTPUT_ROOT = process.env.OUTPUT_DIR || path.join('data', 'en_bible')
if (!API_KEY) {
console.error('Missing API_BIBLE_KEY in environment')
process.exit(1)
}
// Simple throttling and backoff
const baseDelayMs = 350 // ~3 requests/second baseline
function sleep(ms: number) { return new Promise(res => setTimeout(res, ms)) }
async function requestJson(url: string, init: RequestInit = {}, attempt = 0): Promise<any> {
await sleep(baseDelayMs)
const res = await fetch(url, {
...init,
headers: {
'accept': 'application/json',
'api-key': API_KEY,
...(init.headers || {})
}
})
if (res.status === 429) {
const retry = parseInt(res.headers.get('retry-after') || '0', 10)
const wait = Math.max(1000, (retry || 1) * 1000)
if (attempt < 6) {
console.warn(`429 rate limited. Waiting ${wait}ms and retrying...`)
await sleep(wait)
return requestJson(url, init, attempt + 1)
}
}
if (!res.ok) {
const body = await res.text()
throw new Error(`HTTP ${res.status} for ${url}: ${body}`)
}
return res.json()
}
type BibleMeta = { id: string; name: string; abbreviation: string; language: { id: string; name: string } }
async function resolveBible(): Promise<BibleMeta> {
if (FORCE_BIBLE_ID) {
const data = await requestJson(`${API_BASE}/v1/bibles/${FORCE_BIBLE_ID}`)
return data.data as BibleMeta
}
const list = await requestJson(`${API_BASE}/v1/bibles?language=eng`) // English
const bibles: BibleMeta[] = list.data || []
let chosen = bibles.find(b => (b.abbreviation || '').toUpperCase() === PREF_ABBR.toUpperCase())
if (!chosen) chosen = bibles[0]
if (!chosen) throw new Error('No English bibles found via API')
return chosen
}
type Book = { id: string; name: string; abbreviation: string; ord: number }
type Chapter = { id: string; number: string }
async function fetchBooks(bibleId: string): Promise<Book[]> {
const resp = await requestJson(`${API_BASE}/v1/bibles/${bibleId}/books`)
const data = resp.data || []
return data.map((b: any, i: number) => ({ id: b.id, name: b.name, abbreviation: b.abbreviation, ord: b.order || (i + 1) }))
}
async function fetchChapters(bibleId: string, bookId: string): Promise<Chapter[]> {
const resp = await requestJson(`${API_BASE}/v1/bibles/${bibleId}/books/${bookId}/chapters`)
const data = resp.data || []
return data.map((c: any) => ({ id: c.id, number: c.number }))
}
// Fetch all verse IDs for a chapter, then fetch each verse text (contentType=text)
async function fetchChapterVerses(bibleId: string, chapterId: string): Promise<{ verseNum: number; text: string }[]> {
const resp = await requestJson(`${API_BASE}/v1/bibles/${bibleId}/chapters/${chapterId}/verses`)
const verses: any[] = resp.data || []
const results: { verseNum: number; text: string }[] = []
for (const v of verses) {
const vId = v.id
// Respect rate limits while fetching verse content
const vResp = await requestJson(`${API_BASE}/v1/bibles/${bibleId}/verses/${vId}?content-type=text&include-notes=false&include-titles=false&include-chapter-numbers=false&include-verse-numbers=false&include-verse-spans=false`)
const text: string = vResp?.data?.content?.trim?.() || vResp?.data?.content || ''
// Extract verse number from reference when available, fallback to sequence
const num = parseInt((v.reference || '').split(':')[1] || v.verseCount || v.position || results.length + 1, 10)
results.push({ verseNum: Number.isFinite(num) ? num : (results.length + 1), text })
}
// Sort by verse number just in case
results.sort((a, b) => a.verseNum - b.verseNum)
return results
}
function ensureDir(p: string) { fs.mkdirSync(p, { recursive: true }) }
function writeJson(file: string, obj: any) { ensureDir(path.dirname(file)); fs.writeFileSync(file, JSON.stringify(obj, null, 2), 'utf-8') }
function exists(p: string) { try { fs.accessSync(p); return true } catch { return false } }
async function main() {
const bible = await resolveBible()
console.log(`Using Bible: ${bible.name} (${bible.abbreviation}) [${bible.id}]`)
const outDir = path.join(OUTPUT_ROOT, bible.abbreviation.toUpperCase())
ensureDir(outDir)
const books = await fetchBooks(bible.id)
// Partition into OT/NT by order threshold (first 39 = OT)
const otBooks = books.filter(b => b.ord <= 39)
const ntBooks = books.filter(b => b.ord > 39)
const buildTestament = async (subset: Book[], label: 'Old Testament' | 'New Testament') => {
const result: any = { testament: label, books: [] as any[] }
for (const b of subset) {
console.log(`Book: ${b.name}`)
const bookOutDir = path.join(outDir, b.abbreviation || b.name)
ensureDir(bookOutDir)
const chs = await fetchChapters(bible.id, b.id)
const chaptersArr: any[] = []
for (const ch of chs) {
const cacheFile = path.join(bookOutDir, `chapter-${ch.number}.json`)
if (exists(cacheFile)) {
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'))
chaptersArr.push(cached)
continue
}
console.log(` Chapter ${ch.number}`)
const verses = await fetchChapterVerses(bible.id, ch.id)
const chapterObj = { chapterNum: parseInt(ch.number, 10), verses }
writeJson(cacheFile, chapterObj)
chaptersArr.push(chapterObj)
}
result.books.push({ name: b.name, chapters: chaptersArr })
}
return result
}
const ot = await buildTestament(otBooks, 'Old Testament')
const nt = await buildTestament(ntBooks, 'New Testament')
const otFile = path.join(outDir, 'old_testament.json')
const ntFile = path.join(outDir, 'new_testament.json')
writeJson(otFile, ot)
writeJson(ntFile, nt)
console.log('Wrote:', otFile)
console.log('Wrote:', ntFile)
console.log('\nNext: import into the versioned schema using scripts/import-romanian-versioned.ts with LANG_CODE=en and TRANSLATION_CODE matching', bible.abbreviation)
}
main().catch(err => {
console.error('Fetch failed:', err)
process.exit(1)
})

View File

@@ -0,0 +1,281 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
interface ApiBibleBook {
id: string
bibleId: string
abbreviation: string
name: string
nameLong: string
}
interface ApiBibleChapter {
id: string
bibleId: string
bookId: string
number: string
reference: string
}
interface ApiBibleVerse {
id: string
orgId: string
bookId: string
chapterId: string
bibleId: string
reference: string
content: string
verseCount: number
}
interface ApiBibleResponse<T> {
data: T
}
const API_KEY = process.env.API_BIBLE_KEY || '7b42606f8f809e155c9b0742c4f1849b'
const API_BASE = 'https://api.scripture.api.bible/v1'
// English Bible for standard structure
const BIBLE_ID = 'bba9f40183526463-01' // Berean Standard Bible
async function apiFetch<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'api-key': API_KEY
}
})
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
const data = await response.json()
return data
}
function cleanHtmlContent(htmlContent: string): string {
// Remove HTML tags and extract plain text
return htmlContent
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/\s+/g, ' ') // Normalize whitespace
.replace(/^\d+\s*/, '') // Remove verse numbers at start
.trim()
}
function parseVerseNumber(verseId: string): number {
// Extract verse number from ID like "GEN.1.5"
const parts = verseId.split('.')
return parseInt(parts[2]) || 1
}
function parseChapterNumber(chapterId: string): number {
// Extract chapter number from ID like "GEN.1"
const parts = chapterId.split('.')
return parseInt(parts[1]) || 1
}
function getTestament(bookId: string): string {
// Old Testament books (standard order)
const oldTestamentBooks = [
'GEN', 'EXO', 'LEV', 'NUM', 'DEU', 'JOS', 'JDG', 'RUT',
'1SA', '2SA', '1KI', '2KI', '1CH', '2CH', 'EZR', 'NEH',
'EST', 'JOB', 'PSA', 'PRO', 'ECC', 'SNG', 'ISA', 'JER',
'LAM', 'EZK', 'DAN', 'HOS', 'JOL', 'AMO', 'OBA', 'JON',
'MIC', 'NAM', 'HAB', 'ZEP', 'HAG', 'ZEC', 'MAL'
]
return oldTestamentBooks.includes(bookId) ? 'Old Testament' : 'New Testament'
}
function getBookOrderNumber(bookId: string): number {
// Standard Biblical book order
const bookOrder: Record<string, number> = {
// Old Testament
'GEN': 1, 'EXO': 2, 'LEV': 3, 'NUM': 4, 'DEU': 5, 'JOS': 6, 'JDG': 7, 'RUT': 8,
'1SA': 9, '2SA': 10, '1KI': 11, '2KI': 12, '1CH': 13, '2CH': 14, 'EZR': 15, 'NEH': 16,
'EST': 17, 'JOB': 18, 'PSA': 19, 'PRO': 20, 'ECC': 21, 'SNG': 22, 'ISA': 23, 'JER': 24,
'LAM': 25, 'EZK': 26, 'DAN': 27, 'HOS': 28, 'JOL': 29, 'AMO': 30, 'OBA': 31, 'JON': 32,
'MIC': 33, 'NAM': 34, 'HAB': 35, 'ZEP': 36, 'HAG': 37, 'ZEC': 38, 'MAL': 39,
// New Testament
'MAT': 40, 'MRK': 41, 'LUK': 42, 'JHN': 43, 'ACT': 44, 'ROM': 45, '1CO': 46, '2CO': 47,
'GAL': 48, 'EPH': 49, 'PHP': 50, 'COL': 51, '1TH': 52, '2TH': 53, '1TI': 54, '2TI': 55,
'TIT': 56, 'PHM': 57, 'HEB': 58, 'JAS': 59, '1PE': 60, '2PE': 61, '1JN': 62, '2JN': 63,
'3JN': 64, 'JUD': 65, 'REV': 66
}
return bookOrder[bookId] || 999
}
async function importFromApiBible() {
console.log('Starting API.Bible import...')
try {
// Get all books for the Bible
console.log('Fetching books...')
const booksResponse = await apiFetch<ApiBibleResponse<ApiBibleBook[]>>(`/bibles/${BIBLE_ID}/books`)
const books = booksResponse.data.filter(book =>
book.id !== 'INT' && // Skip introduction
!book.id.includes('intro') // Skip intro chapters
)
console.log(`Found ${books.length} books`)
let totalVersesImported = 0
for (const book of books.slice(0, 10)) { // Import first 10 books
console.log(`Processing ${book.name} (${book.id})...`)
const orderNum = getBookOrderNumber(book.id)
const testament = getTestament(book.id)
// Create book
const createdBook = await prisma.bibleBook.upsert({
where: { id: orderNum },
update: {},
create: {
id: orderNum,
name: book.name,
testament: testament,
orderNum: orderNum
}
})
// Get chapters for this book
const chaptersResponse = await apiFetch<ApiBibleResponse<ApiBibleChapter[]>>(`/bibles/${BIBLE_ID}/books/${book.id}/chapters`)
const chapters = chaptersResponse.data.filter(chapter =>
chapter.number !== 'intro' && // Skip introduction chapters
!isNaN(parseInt(chapter.number)) // Only numeric chapters
)
console.log(` Found ${chapters.length} chapters`)
for (const chapter of chapters.slice(0, 5)) { // Import first 5 chapters
const chapterNum = parseChapterNumber(chapter.id)
console.log(` Processing chapter ${chapterNum}...`)
// Create chapter
const createdChapter = await prisma.bibleChapter.upsert({
where: {
bookId_chapterNum: {
bookId: orderNum,
chapterNum: chapterNum
}
},
update: {},
create: {
bookId: orderNum,
chapterNum: chapterNum
}
})
// Get verses for this chapter
const versesResponse = await apiFetch<ApiBibleResponse<ApiBibleVerse[]>>(`/bibles/${BIBLE_ID}/chapters/${chapter.id}/verses`)
console.log(` Found ${versesResponse.data.length} verses`)
// Process all verses
const allVerses = versesResponse.data
for (let i = 0; i < allVerses.length; i += 10) {
const verseBatch = allVerses.slice(i, i + 10)
for (const verseRef of verseBatch) {
try {
// Get full verse content
const verseResponse = await apiFetch<ApiBibleResponse<ApiBibleVerse>>(`/bibles/${BIBLE_ID}/verses/${verseRef.id}`)
const verse = verseResponse.data
const verseNum = parseVerseNumber(verse.id)
const cleanText = cleanHtmlContent(verse.content)
if (cleanText.length > 0) {
// Create verse
await prisma.bibleVerse.upsert({
where: {
chapterId_verseNum_version: {
chapterId: createdChapter.id,
verseNum: verseNum,
version: 'EN'
}
},
update: {},
create: {
chapterId: createdChapter.id,
verseNum: verseNum,
text: cleanText,
version: 'EN'
}
})
totalVersesImported++
}
// Rate limiting - small delay between requests
await new Promise(resolve => setTimeout(resolve, 100))
} catch (error) {
console.warn(` Warning: Failed to fetch verse ${verseRef.id}:`, error)
}
}
}
}
}
console.log(`\nAPI.Bible import completed! Imported ${totalVersesImported} verses.`)
// Create search function for English content
console.log('Creating English search function...')
await prisma.$executeRaw`
CREATE OR REPLACE FUNCTION search_verses_en(search_query TEXT, limit_count INT DEFAULT 10)
RETURNS TABLE(
verse_id TEXT,
book_name TEXT,
chapter_num INT,
verse_num INT,
verse_text TEXT,
rank REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
v.id::TEXT,
b.name,
c."chapterNum",
v."verseNum",
v.text,
CASE
WHEN v.text ILIKE '%' || search_query || '%' THEN 1.0
ELSE ts_rank(to_tsvector('english', v.text), plainto_tsquery('english', search_query))
END as rank
FROM "BibleVerse" v
JOIN "BibleChapter" c ON v."chapterId" = c.id
JOIN "BibleBook" b ON c."bookId" = b.id
WHERE v.version = 'EN'
AND (v.text ILIKE '%' || search_query || '%'
OR to_tsvector('english', v.text) @@ plainto_tsquery('english', search_query))
ORDER BY rank DESC, b."orderNum", c."chapterNum", v."verseNum"
LIMIT limit_count;
END;
$$ LANGUAGE plpgsql;
`
console.log('English search function created successfully!')
} catch (error) {
console.error('Error importing from API.Bible:', error)
throw error
}
}
// Run the import
importFromApiBible()
.then(() => {
console.log('API.Bible import completed successfully!')
process.exit(0)
})
.catch((error) => {
console.error('Import failed:', error)
process.exit(1)
})
.finally(() => prisma.$disconnect())

184
scripts/old/import-bible.ts Normal file
View File

@@ -0,0 +1,184 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// Sample Bible data - Genesis 1:1-5 for demonstration
const sampleBibleData = {
books: [
{
id: 1,
name: "Geneza",
testament: "Vechiul Testament",
chapters: [
{
number: 1,
verses: [
{
number: 1,
text: "La început Dumnezeu a făcut cerurile și pământul."
},
{
number: 2,
text: "Pământul era pustiu și gol; peste adâncuri era întuneric, și Duhul lui Dumnezeu Se mișca pe deasupra apelor."
},
{
number: 3,
text: "Dumnezeu a zis: \"Să fie lumină!\" Și a fost lumină."
},
{
number: 4,
text: "Dumnezeu a văzut că lumina era bună; și Dumnezeu a despărțit lumina de întuneric."
},
{
number: 5,
text: "Dumnezeu a numit lumina zi, iar întunericul l-a numit noapte. Astfel, a fost o seară, și a fost o dimineață: ziua întâi."
}
]
}
]
},
{
id: 2,
name: "Exodul",
testament: "Vechiul Testament",
chapters: [
{
number: 1,
verses: [
{
number: 1,
text: "Iată numele fiilor lui Israel care au intrat în Egipt cu Iacob și au intrat fiecare cu familia lui:"
}
]
}
]
},
{
id: 40,
name: "Matei",
testament: "Noul Testament",
chapters: [
{
number: 1,
verses: [
{
number: 1,
text: "Cartea neamului lui Isus Hristos, fiul lui David, fiul lui Avraam."
}
]
}
]
}
]
}
async function importBible() {
console.log('Starting Bible import...')
try {
for (const book of sampleBibleData.books) {
console.log(`Importing ${book.name}...`)
// Create book
await prisma.bibleBook.upsert({
where: { id: book.id },
update: {},
create: {
id: book.id,
name: book.name,
testament: book.testament,
orderNum: book.id
}
})
// Create chapters and verses
for (const chapter of book.chapters) {
const createdChapter = await prisma.bibleChapter.upsert({
where: {
bookId_chapterNum: {
bookId: book.id,
chapterNum: chapter.number
}
},
update: {},
create: {
bookId: book.id,
chapterNum: chapter.number
}
})
// Create verses
for (const verse of chapter.verses) {
await prisma.bibleVerse.upsert({
where: {
chapterId_verseNum_version: {
chapterId: createdChapter.id,
verseNum: verse.number,
version: 'RO'
}
},
update: {},
create: {
chapterId: createdChapter.id,
verseNum: verse.number,
text: verse.text,
version: 'RO'
}
})
}
}
}
console.log('Bible import completed successfully!')
// Create search function after import
await prisma.$executeRaw`
CREATE OR REPLACE FUNCTION search_verses(search_query TEXT, limit_count INT DEFAULT 10)
RETURNS TABLE(
verse_id TEXT,
book_name TEXT,
chapter_num INT,
verse_num INT,
verse_text TEXT,
rank REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
v.id::TEXT,
b.name,
c."chapterNum",
v."verseNum",
v.text,
CASE
WHEN v.text ILIKE '%' || search_query || '%' THEN 1.0
ELSE 0.5
END as rank
FROM "BibleVerse" v
JOIN "BibleChapter" c ON v."chapterId" = c.id
JOIN "BibleBook" b ON c."bookId" = b.id
WHERE v.text ILIKE '%' || search_query || '%'
ORDER BY rank DESC, b."orderNum", c."chapterNum", v."verseNum"
LIMIT limit_count;
END;
$$ LANGUAGE plpgsql;
`
console.log('Search function created successfully!')
} catch (error) {
console.error('Error importing Bible:', error)
throw error
}
}
importBible()
.then(() => {
console.log('Import process completed')
process.exit(0)
})
.catch((error) => {
console.error('Import failed:', error)
process.exit(1)
})
.finally(() => prisma.$disconnect())

View File

@@ -0,0 +1,140 @@
import { PrismaClient } from '@prisma/client'
import fs from 'fs'
import path from 'path'
const prisma = new PrismaClient()
interface Verse { verseNum: number; text: string }
interface Chapter { chapterNum: number; verses: Verse[] }
interface Book { name: string; chapters: Chapter[] }
interface TestamentFile { testament: string; books: Book[] }
function loadJson(file: string): TestamentFile {
return JSON.parse(fs.readFileSync(file, 'utf-8'))
}
function getBookKeyEn(name: string): string {
const map: Record<string, string> = {
'Genesis': 'genesis', 'Exodus': 'exodus', 'Leviticus': 'leviticus', 'Numbers': 'numbers', 'Deuteronomy': 'deuteronomy',
'Joshua': 'joshua', 'Judges': 'judges', 'Ruth': 'ruth', '1 Samuel': '1_samuel', '2 Samuel': '2_samuel',
'1 Kings': '1_kings', '2 Kings': '2_kings', '1 Chronicles': '1_chronicles', '2 Chronicles': '2_chronicles',
'Ezra': 'ezra', 'Nehemiah': 'nehemiah', 'Esther': 'esther', 'Job': 'job', 'Psalms': 'psalms',
'Proverbs': 'proverbs', 'Ecclesiastes': 'ecclesiastes', 'Song of Songs': 'song_of_songs', 'Isaiah': 'isaiah',
'Jeremiah': 'jeremiah', 'Lamentations': 'lamentations', 'Ezekiel': 'ezekiel', 'Daniel': 'daniel',
'Hosea': 'hosea', 'Joel': 'joel', 'Amos': 'amos', 'Obadiah': 'obadiah', 'Jonah': 'jonah', 'Micah': 'micah',
'Nahum': 'nahum', 'Habakkuk': 'habakkuk', 'Zephaniah': 'zephaniah', 'Haggai': 'haggai', 'Zechariah': 'zechariah', 'Malachi': 'malachi',
'Matthew': 'matthew', 'Mark': 'mark', 'Luke': 'luke', 'John': 'john', 'Acts': 'acts', 'Romans': 'romans',
'1 Corinthians': '1_corinthians', '2 Corinthians': '2_corinthians', 'Galatians': 'galatians', 'Ephesians': 'ephesians', 'Philippians': 'philippians', 'Colossians': 'colossians',
'1 Thessalonians': '1_thessalonians', '2 Thessalonians': '2_thessalonians', '1 Timothy': '1_timothy', '2 Timothy': '2_timothy', 'Titus': 'titus', 'Philemon': 'philemon',
'Hebrews': 'hebrews', 'James': 'james', '1 Peter': '1_peter', '2 Peter': '2_peter', '1 John': '1_john', '2 John': '2_john', '3 John': '3_john', 'Jude': 'jude', 'Revelation': 'revelation'
}
return map[name] || name.toLowerCase().replace(/\s+/g, '_')
}
function getOrderFromList(name: string, list: string[]): number {
const idx = list.indexOf(name)
return idx >= 0 ? idx + 1 : 999
}
async function main() {
try {
const abbr = (process.env.EN_ABBR || 'BSB').toUpperCase()
const inputDir = process.env.INPUT_DIR || path.join('data', 'en_bible', abbr)
const lang = 'en'
const otPath = path.join(inputDir, 'old_testament.json')
const ntPath = path.join(inputDir, 'new_testament.json')
if (!fs.existsSync(otPath) || !fs.existsSync(ntPath)) {
throw new Error(`Missing OT/NT JSON at ${inputDir}. Run fetch-english-bible.ts first.`)
}
// Upsert English version
const englishVersion = await prisma.bibleVersion.upsert({
where: { abbreviation_language: { abbreviation: abbr, language: lang } },
update: {},
create: {
name: abbr,
abbreviation: abbr,
language: lang,
description: `English Bible (${abbr})`,
isDefault: true
}
})
const ot = loadJson(otPath)
const nt = loadJson(ntPath)
const canon = [...ot.books.map(b => b.name), ...nt.books.map(b => b.name)]
let importedBooks = 0
let importedChapters = 0
let importedVerses = 0
async function importTestament(test: TestamentFile) {
for (const book of test.books) {
const orderNum = getOrderFromList(book.name, canon)
const testament = test.testament
const bookKey = getBookKeyEn(book.name)
const createdBook = await prisma.bibleBook.upsert({
where: {
versionId_orderNum: {
versionId: englishVersion.id,
orderNum
}
},
update: {},
create: {
versionId: englishVersion.id,
name: book.name,
testament,
orderNum,
bookKey
}
})
importedBooks++
for (const chapter of book.chapters) {
const createdChapter = await prisma.bibleChapter.upsert({
where: {
bookId_chapterNum: {
bookId: createdBook.id,
chapterNum: chapter.chapterNum
}
},
update: {},
create: { bookId: createdBook.id, chapterNum: chapter.chapterNum }
})
importedChapters++
// Deduplicate verses by verseNum
const unique = new Map<number, string>()
for (const v of chapter.verses) {
if (!unique.has(v.verseNum)) unique.set(v.verseNum, v.text)
}
const versesData = Array.from(unique.entries()).map(([num, text]) => ({
chapterId: createdChapter.id,
verseNum: num,
text
}))
if (versesData.length > 0) {
await prisma.bibleVerse.createMany({ data: versesData, skipDuplicates: true })
importedVerses += versesData.length
}
}
}
}
await importTestament(ot)
await importTestament(nt)
console.log(`Imported ${importedBooks} books, ${importedChapters} chapters, ${importedVerses} verses for ${abbr}.`)
} catch (e) {
console.error('English JSON import failed:', e)
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
main()

View File

@@ -0,0 +1,286 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
interface ApiBibleBook {
id: string
bibleId: string
abbreviation: string
name: string
nameLong: string
}
interface ApiBibleChapter {
id: string
bibleId: string
bookId: string
number: string
reference: string
}
interface ApiBibleVerse {
id: string
orgId: string
bookId: string
chapterId: string
bibleId: string
reference: string
content: string
verseCount: number
}
interface ApiBibleResponse<T> {
data: T
}
const API_KEY = process.env.API_BIBLE_KEY || '7b42606f8f809e155c9b0742c4f1849b'
const API_BASE = 'https://api.scripture.api.bible/v1'
const BIBLE_ID = 'bba9f40183526463-01' // Berean Standard Bible
async function apiFetch<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'api-key': API_KEY
}
})
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
const data = await response.json()
return data
}
function cleanHtmlContent(htmlContent: string): string {
return htmlContent
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/\s+/g, ' ') // Normalize whitespace
.replace(/^\d+\s*/, '') // Remove verse numbers at start
.trim()
}
function parseVerseNumber(verseId: string): number {
const parts = verseId.split('.')
return parseInt(parts[2]) || 1
}
function parseChapterNumber(chapterId: string): number {
const parts = chapterId.split('.')
return parseInt(parts[1]) || 1
}
function getTestament(bookId: string): string {
const oldTestamentBooks = [
'GEN', 'EXO', 'LEV', 'NUM', 'DEU', 'JOS', 'JDG', 'RUT',
'1SA', '2SA', '1KI', '2KI', '1CH', '2CH', 'EZR', 'NEH',
'EST', 'JOB', 'PSA', 'PRO', 'ECC', 'SNG', 'ISA', 'JER',
'LAM', 'EZK', 'DAN', 'HOS', 'JOL', 'AMO', 'OBA', 'JON',
'MIC', 'NAM', 'HAB', 'ZEP', 'HAG', 'ZEC', 'MAL'
]
return oldTestamentBooks.includes(bookId) ? 'Old Testament' : 'New Testament'
}
function getBookOrderNumber(bookId: string): number {
const bookOrder: Record<string, number> = {
// Old Testament
'GEN': 1, 'EXO': 2, 'LEV': 3, 'NUM': 4, 'DEU': 5, 'JOS': 6, 'JDG': 7, 'RUT': 8,
'1SA': 9, '2SA': 10, '1KI': 11, '2KI': 12, '1CH': 13, '2CH': 14, 'EZR': 15, 'NEH': 16,
'EST': 17, 'JOB': 18, 'PSA': 19, 'PRO': 20, 'ECC': 21, 'SNG': 22, 'ISA': 23, 'JER': 24,
'LAM': 25, 'EZK': 26, 'DAN': 27, 'HOS': 28, 'JOL': 29, 'AMO': 30, 'OBA': 31, 'JON': 32,
'MIC': 33, 'NAM': 34, 'HAB': 35, 'ZEP': 36, 'HAG': 37, 'ZEC': 38, 'MAL': 39,
// New Testament
'MAT': 40, 'MRK': 41, 'LUK': 42, 'JHN': 43, 'ACT': 44, 'ROM': 45, '1CO': 46, '2CO': 47,
'GAL': 48, 'EPH': 49, 'PHP': 50, 'COL': 51, '1TH': 52, '2TH': 53, '1TI': 54, '2TI': 55,
'TIT': 56, 'PHM': 57, 'HEB': 58, 'JAS': 59, '1PE': 60, '2PE': 61, '1JN': 62, '2JN': 63,
'3JN': 64, 'JUD': 65, 'REV': 66
}
return bookOrder[bookId] || 999
}
function getBookKey(bookId: string): string {
const keyMap: Record<string, string> = {
'GEN': 'genesis', 'EXO': 'exodus', 'LEV': 'leviticus', 'NUM': 'numbers', 'DEU': 'deuteronomy',
'JOS': 'joshua', 'JDG': 'judges', 'RUT': 'ruth', '1SA': '1_samuel', '2SA': '2_samuel',
'1KI': '1_kings', '2KI': '2_kings', '1CH': '1_chronicles', '2CH': '2_chronicles',
'EZR': 'ezra', 'NEH': 'nehemiah', 'EST': 'esther', 'JOB': 'job', 'PSA': 'psalms',
'PRO': 'proverbs', 'ECC': 'ecclesiastes', 'SNG': 'song_of_songs', 'ISA': 'isaiah',
'JER': 'jeremiah', 'LAM': 'lamentations', 'EZK': 'ezekiel', 'DAN': 'daniel',
'HOS': 'hosea', 'JOL': 'joel', 'AMO': 'amos', 'OBA': 'obadiah', 'JON': 'jonah',
'MIC': 'micah', 'NAM': 'nahum', 'HAB': 'habakkuk', 'ZEP': 'zephaniah',
'HAG': 'haggai', 'ZEC': 'zechariah', 'MAL': 'malachi',
'MAT': 'matthew', 'MRK': 'mark', 'LUK': 'luke', 'JHN': 'john', 'ACT': 'acts',
'ROM': 'romans', '1CO': '1_corinthians', '2CO': '2_corinthians', 'GAL': 'galatians',
'EPH': 'ephesians', 'PHP': 'philippians', 'COL': 'colossians', '1TH': '1_thessalonians',
'2TH': '2_thessalonians', '1TI': '1_timothy', '2TI': '2_timothy', 'TIT': 'titus',
'PHM': 'philemon', 'HEB': 'hebrews', 'JAS': 'james', '1PE': '1_peter', '2PE': '2_peter',
'1JN': '1_john', '2JN': '2_john', '3JN': '3_john', 'JUD': 'jude', 'REV': 'revelation'
}
return keyMap[bookId] || bookId.toLowerCase()
}
async function importEnglishBible() {
console.log('Starting English Bible import with versioned schema...')
try {
// Step 1: Create English Bible version
console.log('Creating English Bible version...')
const englishVersion = await prisma.bibleVersion.upsert({
where: {
abbreviation_language: {
abbreviation: 'BSB',
language: 'en'
}
},
update: {},
create: {
name: 'Berean Standard Bible',
abbreviation: 'BSB',
language: 'en',
description: 'The Berean Standard Bible in English',
isDefault: true
}
})
console.log(`Created English version: ${englishVersion.id}`)
// Step 2: Get all books for the Bible
console.log('Fetching books from API.Bible...')
const booksResponse = await apiFetch<ApiBibleResponse<ApiBibleBook[]>>(`/bibles/${BIBLE_ID}/books`)
const books = booksResponse.data.filter(book =>
book.id !== 'INT' && // Skip introduction
!book.id.includes('intro') // Skip intro chapters
)
console.log(`Found ${books.length} books`)
let totalVersesImported = 0
// Import first 5 books to respect API rate limits
for (const book of books.slice(0, 5)) {
console.log(`Processing ${book.name} (${book.id})...`)
const orderNum = getBookOrderNumber(book.id)
const testament = getTestament(book.id)
const bookKey = getBookKey(book.id)
// Create or update book
const createdBook = await prisma.bibleBook.upsert({
where: {
versionId_orderNum: {
versionId: englishVersion.id,
orderNum: orderNum
}
},
update: {},
create: {
versionId: englishVersion.id,
name: book.name,
testament: testament,
orderNum: orderNum,
bookKey: bookKey
}
})
// Get chapters for this book
const chaptersResponse = await apiFetch<ApiBibleResponse<ApiBibleChapter[]>>(`/bibles/${BIBLE_ID}/books/${book.id}/chapters`)
const chapters = chaptersResponse.data.filter(chapter =>
chapter.number !== 'intro' && // Skip introduction chapters
!isNaN(parseInt(chapter.number)) // Only numeric chapters
)
console.log(` Found ${chapters.length} chapters`)
// Import first 3 chapters to respect API rate limits
for (const chapter of chapters.slice(0, 3)) {
const chapterNum = parseChapterNumber(chapter.id)
console.log(` Processing chapter ${chapterNum}...`)
// Create or update chapter
const createdChapter = await prisma.bibleChapter.upsert({
where: {
bookId_chapterNum: {
bookId: createdBook.id,
chapterNum: chapterNum
}
},
update: {},
create: {
bookId: createdBook.id,
chapterNum: chapterNum
}
})
// Get verses for this chapter
const versesResponse = await apiFetch<ApiBibleResponse<ApiBibleVerse[]>>(`/bibles/${BIBLE_ID}/chapters/${chapter.id}/verses`)
console.log(` Found ${versesResponse.data.length} verses`)
// Process verses in smaller batches to respect API rate limits
const limitedVerses = versesResponse.data.slice(0, 15) // Limit to first 15 verses per chapter
for (let i = 0; i < limitedVerses.length; i += 5) {
const verseBatch = limitedVerses.slice(i, i + 5)
for (const verseRef of verseBatch) {
try {
// Get full verse content
const verseResponse = await apiFetch<ApiBibleResponse<ApiBibleVerse>>(`/bibles/${BIBLE_ID}/verses/${verseRef.id}`)
const verse = verseResponse.data
const verseNum = parseVerseNumber(verse.id)
const cleanText = cleanHtmlContent(verse.content)
if (cleanText.length > 0) {
// Create or update verse
await prisma.bibleVerse.upsert({
where: {
chapterId_verseNum: {
chapterId: createdChapter.id,
verseNum: verseNum
}
},
update: { text: cleanText },
create: {
chapterId: createdChapter.id,
verseNum: verseNum,
text: cleanText
}
})
totalVersesImported++
}
// Rate limiting - longer delay between requests to respect API limits
await new Promise(resolve => setTimeout(resolve, 300))
} catch (error) {
console.warn(` Warning: Failed to fetch verse ${verseRef.id}:`, error)
}
}
}
}
}
console.log(`\nEnglish Bible import completed! Imported ${totalVersesImported} verses.`)
} catch (error) {
console.error('Error importing English Bible:', error)
throw error
}
}
// Run the import
importEnglishBible()
.then(() => {
console.log('English Bible import completed successfully!')
process.exit(0)
})
.catch((error) => {
console.error('Import failed:', error)
process.exit(1)
})
.finally(() => prisma.$disconnect())

View File

@@ -0,0 +1,305 @@
import { PrismaClient } from '@prisma/client'
import * as fs from 'fs'
import * as path from 'path'
const prisma = new PrismaClient()
// Book name mappings from Romanian to standardized names
const BOOK_MAPPINGS: Record<string, { name: string; abbreviation: string; testament: string; orderNum: number }> = {
'Geneza': { name: 'Geneza', abbreviation: 'GEN', testament: 'OT', orderNum: 1 },
'Exodul': { name: 'Exodul', abbreviation: 'EXO', testament: 'OT', orderNum: 2 },
'Leviticul': { name: 'Leviticul', abbreviation: 'LEV', testament: 'OT', orderNum: 3 },
'Numeri': { name: 'Numerii', abbreviation: 'NUM', testament: 'OT', orderNum: 4 },
'Deuteronom': { name: 'Deuteronomul', abbreviation: 'DEU', testament: 'OT', orderNum: 5 },
'Iosua': { name: 'Iosua', abbreviation: 'JOS', testament: 'OT', orderNum: 6 },
'Judecători': { name: 'Judecătorii', abbreviation: 'JDG', testament: 'OT', orderNum: 7 },
'Rut': { name: 'Rut', abbreviation: 'RUT', testament: 'OT', orderNum: 8 },
'1 Samuel': { name: '1 Samuel', abbreviation: '1SA', testament: 'OT', orderNum: 9 },
'2 Samuel': { name: '2 Samuel', abbreviation: '2SA', testament: 'OT', orderNum: 10 },
'1 Imparati': { name: '1 Împărați', abbreviation: '1KI', testament: 'OT', orderNum: 11 },
'2 Imparati': { name: '2 Împărați', abbreviation: '2KI', testament: 'OT', orderNum: 12 },
'1 Cronici': { name: '1 Cronici', abbreviation: '1CH', testament: 'OT', orderNum: 13 },
'2 Cronici': { name: '2 Cronici', abbreviation: '2CH', testament: 'OT', orderNum: 14 },
'Ezra': { name: 'Ezra', abbreviation: 'EZR', testament: 'OT', orderNum: 15 },
'Neemia': { name: 'Neemia', abbreviation: 'NEH', testament: 'OT', orderNum: 16 },
'Estera': { name: 'Estera', abbreviation: 'EST', testament: 'OT', orderNum: 17 },
'Iov': { name: 'Iov', abbreviation: 'JOB', testament: 'OT', orderNum: 18 },
'Psalmii': { name: 'Psalmii', abbreviation: 'PSA', testament: 'OT', orderNum: 19 },
'Proverbe': { name: 'Proverbele', abbreviation: 'PRO', testament: 'OT', orderNum: 20 },
'Eclesiastul': { name: 'Eclesiastul', abbreviation: 'ECC', testament: 'OT', orderNum: 21 },
'Cântarea Cântărilor': { name: 'Cântarea Cântărilor', abbreviation: 'SNG', testament: 'OT', orderNum: 22 },
'Isaia': { name: 'Isaia', abbreviation: 'ISA', testament: 'OT', orderNum: 23 },
'Ieremia': { name: 'Ieremia', abbreviation: 'JER', testament: 'OT', orderNum: 24 },
'Plângerile': { name: 'Plângerile', abbreviation: 'LAM', testament: 'OT', orderNum: 25 },
'Ezechiel': { name: 'Ezechiel', abbreviation: 'EZK', testament: 'OT', orderNum: 26 },
'Daniel': { name: 'Daniel', abbreviation: 'DAN', testament: 'OT', orderNum: 27 },
'Osea': { name: 'Osea', abbreviation: 'HOS', testament: 'OT', orderNum: 28 },
'Ioel': { name: 'Ioel', abbreviation: 'JOL', testament: 'OT', orderNum: 29 },
'Amos': { name: 'Amos', abbreviation: 'AMO', testament: 'OT', orderNum: 30 },
'Obadia': { name: 'Obadia', abbreviation: 'OBA', testament: 'OT', orderNum: 31 },
'Iona': { name: 'Iona', abbreviation: 'JON', testament: 'OT', orderNum: 32 },
'Mica': { name: 'Mica', abbreviation: 'MIC', testament: 'OT', orderNum: 33 },
'Naum': { name: 'Naum', abbreviation: 'NAM', testament: 'OT', orderNum: 34 },
'Habacuc': { name: 'Habacuc', abbreviation: 'HAB', testament: 'OT', orderNum: 35 },
'Țefania': { name: 'Țefania', abbreviation: 'ZEP', testament: 'OT', orderNum: 36 },
'Hagai': { name: 'Hagai', abbreviation: 'HAG', testament: 'OT', orderNum: 37 },
'Zaharia': { name: 'Zaharia', abbreviation: 'ZEC', testament: 'OT', orderNum: 38 },
'Maleahi': { name: 'Maleahi', abbreviation: 'MAL', testament: 'OT', orderNum: 39 },
// New Testament
'Matei': { name: 'Matei', abbreviation: 'MAT', testament: 'NT', orderNum: 40 },
'Marcu': { name: 'Marcu', abbreviation: 'MRK', testament: 'NT', orderNum: 41 },
'Luca': { name: 'Luca', abbreviation: 'LUK', testament: 'NT', orderNum: 42 },
'Ioan': { name: 'Ioan', abbreviation: 'JHN', testament: 'NT', orderNum: 43 },
'Faptele Apostolilor': { name: 'Faptele Apostolilor', abbreviation: 'ACT', testament: 'NT', orderNum: 44 },
'Romani': { name: 'Romani', abbreviation: 'ROM', testament: 'NT', orderNum: 45 },
'1 Corinteni': { name: '1 Corinteni', abbreviation: '1CO', testament: 'NT', orderNum: 46 },
'2 Corinteni': { name: '2 Corinteni', abbreviation: '2CO', testament: 'NT', orderNum: 47 },
'Galateni': { name: 'Galateni', abbreviation: 'GAL', testament: 'NT', orderNum: 48 },
'Efeseni': { name: 'Efeseni', abbreviation: 'EPH', testament: 'NT', orderNum: 49 },
'Filipeni': { name: 'Filipeni', abbreviation: 'PHP', testament: 'NT', orderNum: 50 },
'Coloseni': { name: 'Coloseni', abbreviation: 'COL', testament: 'NT', orderNum: 51 },
'1 Tesaloniceni': { name: '1 Tesaloniceni', abbreviation: '1TH', testament: 'NT', orderNum: 52 },
'2 Tesaloniceni': { name: '2 Tesaloniceni', abbreviation: '2TH', testament: 'NT', orderNum: 53 },
'1 Timotei': { name: '1 Timotei', abbreviation: '1TI', testament: 'NT', orderNum: 54 },
'2 Timotei': { name: '2 Timotei', abbreviation: '2TI', testament: 'NT', orderNum: 55 },
'Titus': { name: 'Titus', abbreviation: 'TIT', testament: 'NT', orderNum: 56 },
'Filimon': { name: 'Filimon', abbreviation: 'PHM', testament: 'NT', orderNum: 57 },
'Evrei': { name: 'Evrei', abbreviation: 'HEB', testament: 'NT', orderNum: 58 },
'Iacov': { name: 'Iacov', abbreviation: 'JAS', testament: 'NT', orderNum: 59 },
'1 Petru': { name: '1 Petru', abbreviation: '1PE', testament: 'NT', orderNum: 60 },
'2 Petru': { name: '2 Petru', abbreviation: '2PE', testament: 'NT', orderNum: 61 },
'1 Ioan': { name: '1 Ioan', abbreviation: '1JN', testament: 'NT', orderNum: 62 },
'2 Ioan': { name: '2 Ioan', abbreviation: '2JN', testament: 'NT', orderNum: 63 },
'3 Ioan': { name: '3 Ioan', abbreviation: '3JN', testament: 'NT', orderNum: 64 },
'Iuda': { name: 'Iuda', abbreviation: 'JUD', testament: 'NT', orderNum: 65 },
'Revelaţia': { name: 'Revelația', abbreviation: 'REV', testament: 'NT', orderNum: 66 },
}
interface ParsedVerse {
verseNum: number
text: string
}
interface ParsedChapter {
chapterNum: number
verses: ParsedVerse[]
}
interface ParsedBook {
name: string
chapters: ParsedChapter[]
}
async function parseRomanianBible(filePath: string): Promise<ParsedBook[]> {
console.log(`Reading Romanian Bible from: ${filePath}`)
const content = fs.readFileSync(filePath, 'utf-8')
const lines = content.split('\n')
const books: ParsedBook[] = []
let currentBook: ParsedBook | null = null
let currentChapter: ParsedChapter | null = null
let isInBibleContent = false
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
// Start processing after "VECHIUL TESTAMENT"
if (line === 'VECHIUL TESTAMENT' || line === 'TESTAMENT') {
isInBibleContent = true
continue
}
if (!isInBibleContent) continue
// Book detection: … BookName …
const bookMatch = line.match(/^…\s*(.+?)\s*…$/)
if (bookMatch) {
// Save previous book if exists
if (currentBook && currentBook.chapters.length > 0) {
books.push(currentBook)
}
const bookName = bookMatch[1].trim()
console.log(`Found book: ${bookName}`)
currentBook = {
name: bookName,
chapters: []
}
currentChapter = null
continue
}
// Chapter detection: Capitolul X or CApitoLuL X
const chapterMatch = line.match(/^[cC][aA][pP][iI][tT][oO][lL][uU][lL]\s+(\d+)$/i)
if (chapterMatch && currentBook) {
// Save previous chapter if exists
if (currentChapter && currentChapter.verses.length > 0) {
currentBook.chapters.push(currentChapter)
}
const chapterNum = parseInt(chapterMatch[1])
console.log(` Chapter ${chapterNum}`)
currentChapter = {
chapterNum,
verses: []
}
continue
}
// Verse detection: starts with number
const verseMatch = line.match(/^(\d+)\s+(.+)$/)
if (verseMatch && currentChapter) {
const verseNum = parseInt(verseMatch[1])
let verseText = verseMatch[2].trim()
// Handle paragraph markers
verseText = verseText.replace(/^¶\s*/, '')
// Look ahead for continuation lines (lines that don't start with numbers or special markers)
let j = i + 1
while (j < lines.length) {
const nextLine = lines[j].trim()
// Stop if we hit a new verse, chapter, book, or empty line
if (!nextLine ||
nextLine.match(/^\d+\s/) || // New verse
nextLine.match(/^[cC][aA][pP][iI][tT][oO][lL][uU][lL]\s+\d+$/i) || // New chapter
nextLine.match(/^….*…$/) || // New book
nextLine === 'TESTAMENT') { // Testament marker
break
}
// Add continuation line
verseText += ' ' + nextLine
j++
}
// Clean up the text
verseText = verseText.replace(/\s+/g, ' ').trim()
currentChapter.verses.push({
verseNum,
text: verseText
})
// Skip the lines we've processed
i = j - 1
continue
}
}
// Save the last book and chapter
if (currentChapter && currentChapter.verses.length > 0 && currentBook) {
currentBook.chapters.push(currentChapter)
}
if (currentBook && currentBook.chapters.length > 0) {
books.push(currentBook)
}
console.log(`Parsed ${books.length} books`)
return books
}
async function importRomanianBible() {
try {
console.log('Starting Romanian Bible import...')
// Clear existing data
console.log('Clearing existing data...')
await prisma.bibleVerse.deleteMany()
await prisma.bibleChapter.deleteMany()
await prisma.bibleBook.deleteMany()
// Parse the markdown file
const filePath = path.join(process.cwd(), 'bibles', 'Biblia-Fidela-limba-romana.md')
const books = await parseRomanianBible(filePath)
console.log(`Importing ${books.length} books into database...`)
for (const book of books) {
const bookInfo = BOOK_MAPPINGS[book.name]
if (!bookInfo) {
console.warn(`Warning: No mapping found for book "${book.name}", skipping...`)
continue
}
console.log(`Creating book: ${bookInfo.name}`)
// Create book
const createdBook = await prisma.bibleBook.create({
data: {
id: bookInfo.orderNum,
name: bookInfo.name,
testament: bookInfo.testament,
orderNum: bookInfo.orderNum
}
})
// Create chapters and verses
for (const chapter of book.chapters) {
console.log(` Creating chapter ${chapter.chapterNum} with ${chapter.verses.length} verses`)
const createdChapter = await prisma.bibleChapter.create({
data: {
bookId: createdBook.id,
chapterNum: chapter.chapterNum
}
})
// Create verses in batch (deduplicate by verse number)
const uniqueVerses = chapter.verses.reduce((acc, verse) => {
acc[verse.verseNum] = verse // This will overwrite duplicates
return acc
}, {} as Record<number, ParsedVerse>)
const versesData = Object.values(uniqueVerses).map(verse => ({
chapterId: createdChapter.id,
verseNum: verse.verseNum,
text: verse.text,
version: 'FIDELA'
}))
if (versesData.length > 0) {
await prisma.bibleVerse.createMany({
data: versesData
})
}
}
}
// Print summary
const bookCount = await prisma.bibleBook.count()
const chapterCount = await prisma.bibleChapter.count()
const verseCount = await prisma.bibleVerse.count()
console.log('\n✅ Romanian Bible import completed successfully!')
console.log(`📚 Books imported: ${bookCount}`)
console.log(`📖 Chapters imported: ${chapterCount}`)
console.log(`📝 Verses imported: ${verseCount}`)
} catch (error) {
console.error('❌ Error importing Romanian Bible:', error)
throw error
} finally {
await prisma.$disconnect()
}
}
// Run the import
if (require.main === module) {
importRomanianBible()
.then(() => {
console.log('Import completed successfully!')
process.exit(0)
})
.catch((error) => {
console.error('Import failed:', error)
process.exit(1)
})
}
export { importRomanianBible }

View File

@@ -0,0 +1,378 @@
import { PrismaClient } from '@prisma/client'
import * as fs from 'fs'
import * as path from 'path'
import pdfParse from 'pdf-parse'
const prisma = new PrismaClient()
interface BibleVerse {
book: string
chapter: number
verse: number
text: string
}
interface BibleBook {
name: string
testament: string
orderNum: number
}
// Romanian Bible book names mapping
const romanianBooks: BibleBook[] = [
// Old Testament
{ name: 'Geneza', testament: 'Vechiul Testament', orderNum: 1 },
{ name: 'Exodul', testament: 'Vechiul Testament', orderNum: 2 },
{ name: 'Leviticul', testament: 'Vechiul Testament', orderNum: 3 },
{ name: 'Numerii', testament: 'Vechiul Testament', orderNum: 4 },
{ name: 'Deuteronomul', testament: 'Vechiul Testament', orderNum: 5 },
{ name: 'Iosua', testament: 'Vechiul Testament', orderNum: 6 },
{ name: 'Judecătorii', testament: 'Vechiul Testament', orderNum: 7 },
{ name: 'Rut', testament: 'Vechiul Testament', orderNum: 8 },
{ name: '1 Samuel', testament: 'Vechiul Testament', orderNum: 9 },
{ name: '2 Samuel', testament: 'Vechiul Testament', orderNum: 10 },
{ name: '1 Regi', testament: 'Vechiul Testament', orderNum: 11 },
{ name: '2 Regi', testament: 'Vechiul Testament', orderNum: 12 },
{ name: '1 Cronici', testament: 'Vechiul Testament', orderNum: 13 },
{ name: '2 Cronici', testament: 'Vechiul Testament', orderNum: 14 },
{ name: 'Ezra', testament: 'Vechiul Testament', orderNum: 15 },
{ name: 'Neemia', testament: 'Vechiul Testament', orderNum: 16 },
{ name: 'Estera', testament: 'Vechiul Testament', orderNum: 17 },
{ name: 'Iov', testament: 'Vechiul Testament', orderNum: 18 },
{ name: 'Psalmii', testament: 'Vechiul Testament', orderNum: 19 },
{ name: 'Proverbele', testament: 'Vechiul Testament', orderNum: 20 },
{ name: 'Ecleziastul', testament: 'Vechiul Testament', orderNum: 21 },
{ name: 'Cântarea Cântărilor', testament: 'Vechiul Testament', orderNum: 22 },
{ name: 'Isaia', testament: 'Vechiul Testament', orderNum: 23 },
{ name: 'Ieremia', testament: 'Vechiul Testament', orderNum: 24 },
{ name: 'Plângerile', testament: 'Vechiul Testament', orderNum: 25 },
{ name: 'Ezechiel', testament: 'Vechiul Testament', orderNum: 26 },
{ name: 'Daniel', testament: 'Vechiul Testament', orderNum: 27 },
{ name: 'Osea', testament: 'Vechiul Testament', orderNum: 28 },
{ name: 'Ioel', testament: 'Vechiul Testament', orderNum: 29 },
{ name: 'Amos', testament: 'Vechiul Testament', orderNum: 30 },
{ name: 'Obadia', testament: 'Vechiul Testament', orderNum: 31 },
{ name: 'Iona', testament: 'Vechiul Testament', orderNum: 32 },
{ name: 'Mica', testament: 'Vechiul Testament', orderNum: 33 },
{ name: 'Naum', testament: 'Vechiul Testament', orderNum: 34 },
{ name: 'Habacuc', testament: 'Vechiul Testament', orderNum: 35 },
{ name: 'Ţefania', testament: 'Vechiul Testament', orderNum: 36 },
{ name: 'Hagai', testament: 'Vechiul Testament', orderNum: 37 },
{ name: 'Zaharia', testament: 'Vechiul Testament', orderNum: 38 },
{ name: 'Maleahi', testament: 'Vechiul Testament', orderNum: 39 },
// New Testament
{ name: 'Matei', testament: 'Noul Testament', orderNum: 40 },
{ name: 'Marcu', testament: 'Noul Testament', orderNum: 41 },
{ name: 'Luca', testament: 'Noul Testament', orderNum: 42 },
{ name: 'Ioan', testament: 'Noul Testament', orderNum: 43 },
{ name: 'Faptele Apostolilor', testament: 'Noul Testament', orderNum: 44 },
{ name: 'Romani', testament: 'Noul Testament', orderNum: 45 },
{ name: '1 Corinteni', testament: 'Noul Testament', orderNum: 46 },
{ name: '2 Corinteni', testament: 'Noul Testament', orderNum: 47 },
{ name: 'Galateni', testament: 'Noul Testament', orderNum: 48 },
{ name: 'Efeseni', testament: 'Noul Testament', orderNum: 49 },
{ name: 'Filipeni', testament: 'Noul Testament', orderNum: 50 },
{ name: 'Coloseni', testament: 'Noul Testament', orderNum: 51 },
{ name: '1 Tesaloniceni', testament: 'Noul Testament', orderNum: 52 },
{ name: '2 Tesaloniceni', testament: 'Noul Testament', orderNum: 53 },
{ name: '1 Timotei', testament: 'Noul Testament', orderNum: 54 },
{ name: '2 Timotei', testament: 'Noul Testament', orderNum: 55 },
{ name: 'Tit', testament: 'Noul Testament', orderNum: 56 },
{ name: 'Filimon', testament: 'Noul Testament', orderNum: 57 },
{ name: 'Evrei', testament: 'Noul Testament', orderNum: 58 },
{ name: 'Iacob', testament: 'Noul Testament', orderNum: 59 },
{ name: '1 Petru', testament: 'Noul Testament', orderNum: 60 },
{ name: '2 Petru', testament: 'Noul Testament', orderNum: 61 },
{ name: '1 Ioan', testament: 'Noul Testament', orderNum: 62 },
{ name: '2 Ioan', testament: 'Noul Testament', orderNum: 63 },
{ name: '3 Ioan', testament: 'Noul Testament', orderNum: 64 },
{ name: 'Iuda', testament: 'Noul Testament', orderNum: 65 },
{ name: 'Apocalipsa', testament: 'Noul Testament', orderNum: 66 }
]
function parseRomanianBible(text: string): BibleVerse[] {
const verses: BibleVerse[] = []
// Remove common headers/footers and normalize text
const cleanText = text
.replace(/BIBLIA\s+FIDELA/gi, '')
.replace(/Copyright.*?România/gi, '')
.replace(/Cluj-Napoca.*?\d{4}/gi, '')
.replace(/\d+\s*$/gm, '') // Remove page numbers at end of lines
.replace(/\s+/g, ' ') // Normalize whitespace
.trim()
console.log('Cleaned text preview:', cleanText.substring(0, 2000))
// Look for patterns like "Geneza 1:1" or "1:1" with verse text
// First, split by book names to identify sections
const bookSections: { book: string, content: string }[] = []
let currentContent = cleanText
for (const book of romanianBooks) {
// Look for book name followed by chapter/verse patterns
const bookPattern = new RegExp(`\\b${book.name}\\b`, 'gi')
const bookMatch = currentContent.search(bookPattern)
if (bookMatch !== -1) {
// Extract content for this book (until next book or end)
let nextBookStart = currentContent.length
for (const nextBook of romanianBooks) {
if (nextBook.orderNum > book.orderNum) {
const nextPattern = new RegExp(`\\b${nextBook.name}\\b`, 'gi')
const nextMatch = currentContent.search(nextPattern)
if (nextMatch > bookMatch && nextMatch < nextBookStart) {
nextBookStart = nextMatch
}
}
}
const bookContent = currentContent.substring(bookMatch, nextBookStart)
bookSections.push({ book: book.name, content: bookContent })
}
}
console.log(`Found ${bookSections.length} book sections`)
// Parse each book section
for (const section of bookSections) {
console.log(`Parsing ${section.book}...`)
// Look for chapter:verse patterns like "1:1", "1:2", etc.
const versePattern = /(\d+):(\d+)\s+([^0-9:]+?)(?=\d+:\d+|$)/g
let match
while ((match = versePattern.exec(section.content)) !== null) {
const chapter = parseInt(match[1])
const verse = parseInt(match[2])
const text = match[3].trim()
// Clean up the verse text
const cleanVerseText = text
.replace(/\s+/g, ' ')
.replace(/^\W+|\W+$/g, '') // Remove leading/trailing non-word chars
.trim()
if (cleanVerseText.length > 5) { // Only keep substantial text
verses.push({
book: section.book,
chapter: chapter,
verse: verse,
text: cleanVerseText
})
}
}
// Alternative: look for numbered verses within paragraphs
const numberedVersePattern = /(\d+)\s+([^0-9]+?)(?=\d+\s+|$)/g
let altMatch
let currentChapter = 1
// Try to find chapter indicators
const chapterPattern = /Capitolul\s+(\d+)|^(\d+)$/gm
const chapterMatches = [...section.content.matchAll(chapterPattern)]
if (chapterMatches.length > 0) {
for (const chMatch of chapterMatches) {
currentChapter = parseInt(chMatch[1] || chMatch[2])
// Find content after this chapter marker
const chapterStart = chMatch.index! + chMatch[0].length
let chapterEnd = section.content.length
// Find next chapter marker
for (const nextChMatch of chapterMatches) {
if (nextChMatch.index! > chMatch.index! && nextChMatch.index! < chapterEnd) {
chapterEnd = nextChMatch.index!
}
}
const chapterContent = section.content.substring(chapterStart, chapterEnd)
// Parse verses in this chapter
while ((altMatch = numberedVersePattern.exec(chapterContent)) !== null) {
const verseNum = parseInt(altMatch[1])
const verseText = altMatch[2].trim()
const cleanText = verseText
.replace(/\s+/g, ' ')
.replace(/^\W+|\W+$/g, '')
.trim()
if (cleanText.length > 10) {
verses.push({
book: section.book,
chapter: currentChapter,
verse: verseNum,
text: cleanText
})
}
}
}
}
}
return verses
}
async function importRomanianBible() {
console.log('Starting Romanian Bible import...')
const pdfPath = path.join(process.cwd(), 'bibles', 'Biblia-Fidela-limba-romana.pdf')
if (!fs.existsSync(pdfPath)) {
throw new Error(`PDF file not found at: ${pdfPath}`)
}
console.log('Reading PDF file...')
const pdfBuffer = fs.readFileSync(pdfPath)
const pdfData = await pdfParse(pdfBuffer)
console.log(`PDF parsed. Text length: ${pdfData.text.length} characters`)
console.log('Parsing Bible verses...')
const verses = parseRomanianBible(pdfData.text)
console.log(`Found ${verses.length} verses`)
if (verses.length === 0) {
console.log('No verses found. PDF content preview:')
console.log(pdfData.text.substring(0, 1000))
throw new Error('Could not parse any verses from the PDF')
}
try {
// First, create all books
console.log('Creating Bible books...')
for (const bookData of romanianBooks) {
await prisma.bibleBook.upsert({
where: { id: bookData.orderNum },
update: {},
create: {
id: bookData.orderNum,
name: bookData.name,
testament: bookData.testament,
orderNum: bookData.orderNum
}
})
}
// Group verses by book and chapter
const versesByBook = verses.reduce((acc, verse) => {
if (!acc[verse.book]) acc[verse.book] = {}
if (!acc[verse.book][verse.chapter]) acc[verse.book][verse.chapter] = []
acc[verse.book][verse.chapter].push(verse)
return acc
}, {} as Record<string, Record<number, BibleVerse[]>>)
console.log('Importing verses by book and chapter...')
let totalImported = 0
for (const [bookName, chapters] of Object.entries(versesByBook)) {
const book = romanianBooks.find(b => b.name === bookName)
if (!book) {
console.warn(`Unknown book: ${bookName}`)
continue
}
console.log(`Importing ${bookName}...`)
for (const [chapterNumStr, chapterVerses] of Object.entries(chapters)) {
const chapterNum = parseInt(chapterNumStr)
// Create chapter
const chapter = await prisma.bibleChapter.upsert({
where: {
bookId_chapterNum: {
bookId: book.orderNum,
chapterNum: chapterNum
}
},
update: {},
create: {
bookId: book.orderNum,
chapterNum: chapterNum
}
})
// Create verses
for (const verse of chapterVerses) {
await prisma.bibleVerse.upsert({
where: {
chapterId_verseNum_version: {
chapterId: chapter.id,
verseNum: verse.verse,
version: 'RO'
}
},
update: {},
create: {
chapterId: chapter.id,
verseNum: verse.verse,
text: verse.text,
version: 'RO'
}
})
totalImported++
}
}
}
console.log(`Romanian Bible import completed! Imported ${totalImported} verses.`)
// Create search function
console.log('Creating search function...')
await prisma.$executeRaw`
CREATE OR REPLACE FUNCTION search_verses(search_query TEXT, limit_count INT DEFAULT 10)
RETURNS TABLE(
verse_id TEXT,
book_name TEXT,
chapter_num INT,
verse_num INT,
verse_text TEXT,
rank REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
v.id::TEXT,
b.name,
c."chapterNum",
v."verseNum",
v.text,
CASE
WHEN v.text ILIKE '%' || search_query || '%' THEN 1.0
ELSE ts_rank(to_tsvector('romanian', v.text), plainto_tsquery('romanian', search_query))
END as rank
FROM "BibleVerse" v
JOIN "BibleChapter" c ON v."chapterId" = c.id
JOIN "BibleBook" b ON c."bookId" = b.id
WHERE v.text ILIKE '%' || search_query || '%'
OR to_tsvector('romanian', v.text) @@ plainto_tsquery('romanian', search_query)
ORDER BY rank DESC, b."orderNum", c."chapterNum", v."verseNum"
LIMIT limit_count;
END;
$$ LANGUAGE plpgsql;
`
console.log('Search function created successfully!')
} catch (error) {
console.error('Error importing Romanian Bible:', error)
throw error
}
}
// Run the import
importRomanianBible()
.then(() => {
console.log('Romanian Bible import completed successfully!')
process.exit(0)
})
.catch((error) => {
console.error('Import failed:', error)
process.exit(1)
})
.finally(() => prisma.$disconnect())

View File

@@ -0,0 +1,230 @@
import { PrismaClient } from '@prisma/client'
import fs from 'fs'
import path from 'path'
const prisma = new PrismaClient()
interface BibleData {
testament: string;
books: Array<{
name: string;
chapters: Array<{
chapterNum: number;
verses: Array<{
verseNum: number;
text: string;
}>;
}>;
}>;
}
function getBookKey(bookName: string): string {
const keyMap: Record<string, string> = {
'Geneza': 'genesis',
'Exodul': 'exodus',
'Leviticul': 'leviticus',
'Numerii': 'numbers',
'Deuteronomul': 'deuteronomy',
'Iosua': 'joshua',
'Judecătorii': 'judges',
'Rut': 'ruth',
'1 Samuel': '1_samuel',
'2 Samuel': '2_samuel',
'1 Împăraţi': '1_kings',
'2 Împăraţi': '2_kings',
'1 Cronici': '1_chronicles',
'2 Cronici': '2_chronicles',
'Ezra': 'ezra',
'Neemia': 'nehemiah',
'Estera': 'esther',
'Iov': 'job',
'Psalmii': 'psalms',
'Proverbele': 'proverbs',
'Ecclesiastul': 'ecclesiastes',
'Cântarea Cântărilor': 'song_of_songs',
'Isaia': 'isaiah',
'Ieremia': 'jeremiah',
'Plângerile lui Ieremia': 'lamentations',
'Ezechiel': 'ezekiel',
'Daniel': 'daniel',
'Osea': 'hosea',
'Ioel': 'joel',
'Amos': 'amos',
'Obadia': 'obadiah',
'Iona': 'jonah',
'Mica': 'micah',
'Naum': 'nahum',
'Habacuc': 'habakkuk',
'Ţefania': 'zephaniah',
'Hagai': 'haggai',
'Zaharia': 'zechariah',
'Maleahi': 'malachi',
'Matei': 'matthew',
'Marcu': 'mark',
'Luca': 'luke',
'Ioan': 'john',
'Faptele Apostolilor': 'acts',
'Romani': 'romans',
'1 Corinteni': '1_corinthians',
'2 Corinteni': '2_corinthians',
'Galateni': 'galatians',
'Efeseni': 'ephesians',
'Filipeni': 'philippians',
'Coloseni': 'colossians',
'1 Tesaloniceni': '1_thessalonians',
'2 Tesaloniceni': '2_thessalonians',
'1 Timotei': '1_timothy',
'2 Timotei': '2_timothy',
'Tit': 'titus',
'Filimon': 'philemon',
'Evrei': 'hebrews',
'Iacov': 'james',
'1 Petru': '1_peter',
'2 Petru': '2_peter',
'1 Ioan': '1_john',
'2 Ioan': '2_john',
'3 Ioan': '3_john',
'Iuda': 'jude',
'Apocalipsa': 'revelation',
'Revelaţia': 'revelation',
'Revelația': 'revelation',
'Numeri': 'numbers',
'Deuteronom': 'deuteronomy',
'Judecători': 'judges',
'1 Imparati': '1_kings',
'2 Imparati': '2_kings',
'Proverbe': 'proverbs',
'Țefania': 'zephaniah'
}
return keyMap[bookName] || bookName.toLowerCase().replace(/\s+/g, '_')
}
async function importRomanianBible() {
console.log('Starting Romanian Bible import with versioned schema...')
try {
// Step 1: Create Romanian Bible version
console.log('Creating Romanian Bible version...')
const romanianVersion = await prisma.bibleVersion.upsert({
where: {
abbreviation_language: {
abbreviation: 'CORNILESCU',
language: 'ro'
}
},
update: {},
create: {
name: 'Biblia Cornilescu',
abbreviation: 'CORNILESCU',
language: 'ro',
description: 'Traducerea Cornilescu a Bibliei în limba română',
isDefault: true
}
})
console.log(`Created Romanian version: ${romanianVersion.id}`)
// Step 1.1: Clear any existing Romanian content for this version (idempotent import)
console.log('Clearing existing Romanian version content (if any)...')
await prisma.bibleVerse.deleteMany({
where: { chapter: { book: { versionId: romanianVersion.id } } }
})
await prisma.bibleChapter.deleteMany({
where: { book: { versionId: romanianVersion.id } }
})
await prisma.bibleBook.deleteMany({ where: { versionId: romanianVersion.id } })
// Step 2: Import Old Testament
console.log('Importing Old Testament...')
const otPath = path.join(process.cwd(), 'data', 'old_testament.json')
if (fs.existsSync(otPath)) {
const otData: BibleData = JSON.parse(fs.readFileSync(otPath, 'utf-8'))
await importTestament(romanianVersion.id, otData, 'Old Testament')
} else {
console.log('Old Testament data file not found, skipping...')
}
// Step 3: Import New Testament
console.log('Importing New Testament...')
const ntPath = path.join(process.cwd(), 'data', 'new_testament.json')
if (fs.existsSync(ntPath)) {
const ntData: BibleData = JSON.parse(fs.readFileSync(ntPath, 'utf-8'))
await importTestament(romanianVersion.id, ntData, 'New Testament')
} else {
console.log('New Testament data file not found, skipping...')
}
console.log('Romanian Bible import completed successfully!')
} catch (error) {
console.error('Error importing Romanian Bible:', error)
throw error
}
}
async function importTestament(versionId: string, testamentData: BibleData, testament: string) {
console.log(`Importing ${testament}...`)
let orderNum = testament === 'Old Testament' ? 1 : 40
for (const bookData of testamentData.books) {
console.log(` Processing ${bookData.name}...`)
const bookKey = getBookKey(bookData.name)
// Create book
const book = await prisma.bibleBook.create({
data: {
versionId,
name: bookData.name,
testament,
orderNum,
bookKey
}
})
// Import chapters
for (const chapterData of bookData.chapters) {
console.log(` Chapter ${chapterData.chapterNum}...`)
// Create chapter
const chapter = await prisma.bibleChapter.create({
data: {
bookId: book.id,
chapterNum: chapterData.chapterNum
}
})
// Import verses (dedupe by verseNum, then bulk insert with skipDuplicates)
const uniqueByVerse: Record<number, { verseNum: number; text: string }> = {}
for (const v of chapterData.verses) {
uniqueByVerse[v.verseNum] = { verseNum: v.verseNum, text: v.text }
}
const versesData = Object.values(uniqueByVerse).map(v => ({
chapterId: chapter.id,
verseNum: v.verseNum,
text: v.text
}))
if (versesData.length > 0) {
await prisma.bibleVerse.createMany({ data: versesData, skipDuplicates: true })
}
console.log(` Imported ${chapterData.verses.length} verses`)
}
orderNum++
}
}
// Run the import
importRomanianBible()
.then(() => {
console.log('Romanian Bible import completed successfully!')
process.exit(0)
})
.catch((error) => {
console.error('Import failed:', error)
process.exit(1)
})
.finally(() => prisma.$disconnect())

67
scripts/old/init.sql Normal file
View File

@@ -0,0 +1,67 @@
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "vector";
-- Create cache table for Bible verses
CREATE UNLOGGED TABLE IF NOT EXISTS verse_cache (
key VARCHAR(255) PRIMARY KEY,
value TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- Function to create full-text search index (run after Prisma migration)
CREATE OR REPLACE FUNCTION setup_fulltext_search()
RETURNS void AS $$
BEGIN
-- Create GIN index for full-text search on Bible verses
CREATE INDEX IF NOT EXISTS verse_text_gin_idx ON "BibleVerse" USING gin(to_tsvector('english', text));
CREATE INDEX IF NOT EXISTS verse_text_trigram_idx ON "BibleVerse" USING gin(text gin_trgm_ops);
END;
$$ LANGUAGE plpgsql;
-- Function for verse search with full-text search
CREATE OR REPLACE FUNCTION search_verses(search_query TEXT, limit_count INT DEFAULT 10)
RETURNS TABLE(
verse_id TEXT,
book_name TEXT,
chapter_num INT,
verse_num INT,
verse_text TEXT,
rank REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
v.id::TEXT,
b.name,
c."chapterNum",
v."verseNum",
v.text,
CASE
WHEN to_tsvector('english', v.text) @@ plainto_tsquery('english', search_query) THEN
ts_rank(to_tsvector('english', v.text), plainto_tsquery('english', search_query))
WHEN v.text ILIKE '%' || search_query || '%' THEN 0.5
ELSE similarity(v.text, search_query)
END as rank
FROM "BibleVerse" v
JOIN "BibleChapter" c ON v."chapterId" = c.id
JOIN "BibleBook" b ON c."bookId" = b.id
WHERE
to_tsvector('english', v.text) @@ plainto_tsquery('english', search_query)
OR v.text ILIKE '%' || search_query || '%'
OR similarity(v.text, search_query) > 0.1
ORDER BY rank DESC, b."orderNum", c."chapterNum", v."verseNum"
LIMIT limit_count;
END;
$$ LANGUAGE plpgsql;
-- Session cleanup function
CREATE OR REPLACE FUNCTION cleanup_expired_sessions()
RETURNS void AS $$
BEGIN
DELETE FROM "Session" WHERE "expiresAt" < NOW();
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,187 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// Book key mapping for consistent cross-version identification
const bookKeyMapping: Record<string, string> = {
'Geneza': 'genesis',
'Genesis': 'genesis',
'Exodul': 'exodus',
'Exodus': 'exodus',
'Leviticul': 'leviticus',
'Leviticus': 'leviticus',
'Numerii': 'numbers',
'Numbers': 'numbers',
// Add more as needed
}
function getBookKey(bookName: string): string {
return bookKeyMapping[bookName] || bookName.toLowerCase().replace(/\s+/g, '_')
}
async function migrateToVersionedSchema() {
console.log('Starting migration to versioned Bible schema...')
try {
// Step 1: Create Bible versions
console.log('Creating Bible versions...')
const romanianVersion = await prisma.bibleVersion.upsert({
where: { abbreviation_language: { abbreviation: 'CORNILESCU', language: 'ro' } },
update: {},
create: {
name: 'Cornilescu',
abbreviation: 'CORNILESCU',
language: 'ro',
description: 'Biblia Cornilescu în limba română',
isDefault: true
}
})
const englishVersion = await prisma.bibleVersion.upsert({
where: { abbreviation_language: { abbreviation: 'BSB', language: 'en' } },
update: {},
create: {
name: 'Berean Standard Bible',
abbreviation: 'BSB',
language: 'en',
description: 'English Bible - Berean Standard Bible',
isDefault: true
}
})
console.log(`Created versions: ${romanianVersion.id} (RO), ${englishVersion.id} (EN)`)
// Step 2: Get all existing books and migrate them
console.log('Migrating existing books...')
// Note: This assumes your current BibleBook table has the old structure
// We'll need to query the old structure and create new versioned books
const existingBooks = await prisma.$queryRaw<any[]>`
SELECT id, name, testament, "orderNum" FROM "BibleBook" ORDER BY "orderNum"
`
for (const oldBook of existingBooks) {
const bookKey = getBookKey(oldBook.name)
// Create Romanian version of the book (assuming current data is Romanian)
const roBook = await prisma.bibleBook.create({
data: {
versionId: romanianVersion.id,
name: oldBook.name,
testament: oldBook.testament,
orderNum: oldBook.orderNum,
bookKey: bookKey
}
})
console.log(`Created Romanian book: ${roBook.name} (${bookKey})`)
// Get all chapters for this book and migrate them
const existingChapters = await prisma.$queryRaw<any[]>`
SELECT id, "chapterNum" FROM "BibleChapter" WHERE "bookId" = ${oldBook.id} ORDER BY "chapterNum"
`
for (const oldChapter of existingChapters) {
const newChapter = await prisma.bibleChapter.create({
data: {
bookId: roBook.id,
chapterNum: oldChapter.chapterNum
}
})
// Get all verses for this chapter and migrate them (Romanian only)
const existingVerses = await prisma.$queryRaw<any[]>`
SELECT id, "verseNum", text FROM "BibleVerse"
WHERE "chapterId" = ${oldChapter.id} AND version = 'KJV'
ORDER BY "verseNum"
`
for (const oldVerse of existingVerses) {
await prisma.bibleVerse.create({
data: {
chapterId: newChapter.id,
verseNum: oldVerse.verseNum,
text: oldVerse.text
}
})
}
console.log(` Migrated chapter ${oldChapter.chapterNum} with ${existingVerses.length} verses`)
}
// Also create English version if we have English data
const englishVerses = await prisma.$queryRaw<any[]>`
SELECT COUNT(*) as count FROM "BibleVerse" v
JOIN "BibleChapter" c ON v."chapterId" = c.id
WHERE c."bookId" = ${oldBook.id} AND v.version = 'EN'
`
if (englishVerses[0]?.count > 0) {
// Map English book name (you might need to improve this mapping)
const englishBookName = oldBook.name === 'Geneza' ? 'Genesis' :
oldBook.name === 'Exodul' ? 'Exodus' :
oldBook.name
const enBook = await prisma.bibleBook.create({
data: {
versionId: englishVersion.id,
name: englishBookName,
testament: oldBook.testament,
orderNum: oldBook.orderNum,
bookKey: bookKey
}
})
// Migrate English chapters and verses
for (const oldChapter of existingChapters) {
const enChapter = await prisma.bibleChapter.create({
data: {
bookId: enBook.id,
chapterNum: oldChapter.chapterNum
}
})
const englishVerses = await prisma.$queryRaw<any[]>`
SELECT "verseNum", text FROM "BibleVerse"
WHERE "chapterId" = ${oldChapter.id} AND version = 'EN'
ORDER BY "verseNum"
`
for (const enVerse of englishVerses) {
await prisma.bibleVerse.create({
data: {
chapterId: enChapter.id,
verseNum: enVerse.verseNum,
text: enVerse.text
}
})
}
}
console.log(`Created English book: ${englishBookName}`)
}
}
console.log('Migration completed successfully!')
console.log(`Romanian version ID: ${romanianVersion.id}`)
console.log(`English version ID: ${englishVersion.id}`)
} catch (error) {
console.error('Migration failed:', error)
throw error
}
}
// Run the migration
migrateToVersionedSchema()
.then(() => {
console.log('Migration completed successfully!')
process.exit(0)
})
.catch((error) => {
console.error('Migration failed:', error)
process.exit(1)
})
.finally(() => prisma.$disconnect())

140
scripts/old/optimize-db.sql Normal file
View File

@@ -0,0 +1,140 @@
-- Database Performance Optimization Script
-- Create materialized view for popular verses
CREATE MATERIALIZED VIEW IF NOT EXISTS popular_verses AS
SELECT
v.id,
v.text,
b.name as book_name,
c."chapterNum",
v."verseNum",
COUNT(bm.id) as bookmark_count
FROM "BibleVerse" v
JOIN "BibleChapter" c ON v."chapterId" = c.id
JOIN "BibleBook" b ON c."bookId" = b.id
LEFT JOIN "Bookmark" bm ON v.id = bm."verseId"
GROUP BY v.id, v.text, b.name, c."chapterNum", v."verseNum"
ORDER BY bookmark_count DESC
LIMIT 100;
-- Create unique index on materialized view
CREATE UNIQUE INDEX IF NOT EXISTS popular_verses_id_idx ON popular_verses (id);
-- Function to refresh popular verses materialized view
CREATE OR REPLACE FUNCTION refresh_popular_verses()
RETURNS void AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY popular_verses;
END;
$$ LANGUAGE plpgsql;
-- Create additional performance indexes
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chat_messages_user_created ON "ChatMessage"("userId", "createdAt" DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_bookmarks_user_created ON "Bookmark"("userId", "createdAt" DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_reading_history_user_viewed ON "ReadingHistory"("userId", "viewedAt" DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_prayer_requests_created ON "PrayerRequest"("createdAt" DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_notes_user_created ON "Note"("userId", "createdAt" DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sessions_expires ON "Session"("expiresAt");
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_user_preferences_user_key ON "UserPreference"("userId", "key");
-- Create partial indexes for better performance
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_active_sessions ON "Session"("userId") WHERE "expiresAt" > NOW();
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_recent_prayers ON "PrayerRequest"("createdAt") WHERE "createdAt" > NOW() - INTERVAL '30 days';
-- Function to analyze query performance
CREATE OR REPLACE FUNCTION analyze_query_performance()
RETURNS void AS $$
BEGIN
-- Update table statistics
ANALYZE "User";
ANALYZE "Session";
ANALYZE "BibleBook";
ANALYZE "BibleChapter";
ANALYZE "BibleVerse";
ANALYZE "ChatMessage";
ANALYZE "Bookmark";
ANALYZE "Note";
ANALYZE "PrayerRequest";
ANALYZE "Prayer";
ANALYZE "ReadingHistory";
ANALYZE "UserPreference";
ANALYZE verse_cache;
-- Refresh materialized view
PERFORM refresh_popular_verses();
END;
$$ LANGUAGE plpgsql;
-- Function to cleanup old data
CREATE OR REPLACE FUNCTION cleanup_old_data()
RETURNS void AS $$
BEGIN
-- Clean up expired sessions
DELETE FROM "Session" WHERE "expiresAt" < NOW();
-- Clean up expired cache entries
DELETE FROM verse_cache WHERE expires_at < NOW();
-- Clean up old reading history (older than 1 year)
DELETE FROM "ReadingHistory" WHERE "viewedAt" < NOW() - INTERVAL '1 year';
-- Clean up old anonymous prayer requests (older than 6 months)
DELETE FROM "PrayerRequest"
WHERE "isAnonymous" = true
AND "createdAt" < NOW() - INTERVAL '6 months';
-- Vacuum and analyze after cleanup
VACUUM ANALYZE;
END;
$$ LANGUAGE plpgsql;
-- Create function to monitor database size
CREATE OR REPLACE FUNCTION get_database_stats()
RETURNS TABLE(
table_name TEXT,
row_count BIGINT,
table_size TEXT,
index_size TEXT,
total_size TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
schemaname || '.' || tablename AS table_name,
n_tup_ins - n_tup_del AS row_count,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS table_size,
pg_size_pretty(pg_indexes_size(schemaname||'.'||tablename)) AS index_size,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename) + pg_indexes_size(schemaname||'.'||tablename)) AS total_size
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
END;
$$ LANGUAGE plpgsql;
-- Create function to get slow queries
CREATE OR REPLACE FUNCTION get_slow_queries()
RETURNS TABLE(
query TEXT,
calls BIGINT,
total_time DOUBLE PRECISION,
mean_time DOUBLE PRECISION,
stddev_time DOUBLE PRECISION
) AS $$
BEGIN
RETURN QUERY
SELECT
pg_stat_statements.query,
pg_stat_statements.calls,
pg_stat_statements.total_exec_time,
pg_stat_statements.mean_exec_time,
pg_stat_statements.stddev_exec_time
FROM pg_stat_statements
WHERE pg_stat_statements.mean_exec_time > 100 -- queries taking more than 100ms on average
ORDER BY pg_stat_statements.mean_exec_time DESC
LIMIT 20;
EXCEPTION
WHEN undefined_table THEN
RAISE NOTICE 'pg_stat_statements extension not available';
RETURN;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const SRC = process.env.BSB_MD_PATH || path.join('bibles', 'bible-bsb.md')
const OUT_ABBR = (process.env.EN_ABBR || 'BSB_MD').toUpperCase()
const OUT_DIR = process.env.OUTPUT_DIR || path.join('data','en_bible', OUT_ABBR)
function ensureDir(p){ fs.mkdirSync(p,{recursive:true}) }
function writeJson(file,obj){ ensureDir(path.dirname(file)); fs.writeFileSync(file, JSON.stringify(obj,null,2),'utf-8') }
const BOOKS = [
['Genesis', ['Genesis'],'OT'], ['Exodus',['Exodus'],'OT'], ['Leviticus',['Leviticus'],'OT'], ['Numbers',['Numbers'],'OT'], ['Deuteronomy',['Deuteronomy'],'OT'],
['Joshua',['Joshua'],'OT'], ['Judges',['Judges'],'OT'], ['Ruth',['Ruth'],'OT'], ['1 Samuel',['1\s+Samuel','1\s+Samuel'],'OT'], ['2 Samuel',['2\s+Samuel','2\s+Samuel'],'OT'],
['1 Kings',['1\s+Kings'],'OT'], ['2 Kings',['2\s+Kings'],'OT'], ['1 Chronicles',['1\s+Chronicles'],'OT'], ['2 Chronicles',['2\s+Chronicles'],'OT'],
['Ezra',['Ezra'],'OT'], ['Nehemiah',['Nehemiah'],'OT'], ['Esther',['Esther'],'OT'], ['Job',['Job'],'OT'], ['Psalms',['Psalms|Psalm'],'OT'],
['Proverbs',['Proverbs'],'OT'], ['Ecclesiastes',['Ecclesiastes'],'OT'], ['Song of Songs',['Song\s+of\s+Songs|Song\s+of\s+Solomon'],'OT'], ['Isaiah',['Isaiah'],'OT'],
['Jeremiah',['Jeremiah'],'OT'], ['Lamentations',['Lamentations'],'OT'], ['Ezekiel',['Ezekiel'],'OT'], ['Daniel',['Daniel'],'OT'],
['Hosea',['Hosea'],'OT'], ['Joel',['Joel'],'OT'], ['Amos',['Amos'],'OT'], ['Obadiah',['Obadiah'],'OT'], ['Jonah',['Jonah'],'OT'], ['Micah',['Micah'],'OT'],
['Nahum',['Nahum'],'OT'], ['Habakkuk',['Habakkuk'],'OT'], ['Zephaniah',['Zephaniah'],'OT'], ['Haggai',['Haggai'],'OT'], ['Zechariah',['Zechariah'],'OT'], ['Malachi',['Malachi'],'OT'],
['Matthew',['Matthew'],'NT'], ['Mark',['Mark'],'NT'], ['Luke',['Luke'],'NT'], ['John',['John'],'NT'], ['Acts',['Acts'],'NT'],
['Romans',['Romans'],'NT'], ['1 Corinthians',['1\s+Corinthians'],'NT'], ['2 Corinthians',['2\s+Corinthians'],'NT'], ['Galatians',['Galatians'],'NT'], ['Ephesians',['Ephesians'],'NT'],
['Philippians',['Philippians'],'NT'], ['Colossians',['Colossians'],'NT'], ['1 Thessalonians',['1\s+Thessalonians'],'NT'], ['2 Thessalonians',['2\s+Thessalonians'],'NT'],
['1 Timothy',['1\s+Timothy'],'NT'], ['2 Timothy',['2\s+Timothy'],'NT'], ['Titus',['Titus'],'NT'], ['Philemon',['Philemon'],'NT'],
['Hebrews',['Hebrews'],'NT'], ['James',['James'],'NT'], ['1 Peter',['1\\s+Peter'],'NT'], ['2 Peter',['2\\s+Peter'],'NT'],
['1 John',['1\s+John'],'NT'], ['2 John',['2\s+John'],'NT'], ['3 John',['3\s+John'],'NT'], ['Jude',['Jude'],'NT'], ['Revelation',['Revelation'],'NT']
]
function main(){
if(!fs.existsSync(SRC)) { console.error('Missing source:', SRC); process.exit(1) }
const md = fs.readFileSync(SRC,'utf-8')
// Collect all verse markers across the entire doc
const markers = []
for(const [name, variants] of BOOKS){
const names = variants.join('|')
const re = new RegExp('(?:^|[\\n\\r\\f\\s\\|\\(])(?:'+names+')\\s+(\\d+):(\\d+)', 'gi')
let m
while((m=re.exec(md))!==null){
markers.push({ book:name, chapter:parseInt(m[1],10), verse:parseInt(m[2],10), index:m.index, matchLen:m[0].length })
}
}
if(markers.length===0){ console.error('No verse markers found'); process.exit(1) }
markers.sort((a,b)=>a.index-b.index)
// Build text segments per marker (chapter/verse)
const entries = []
for(let i=0;i<markers.length;i++){
const cur = markers[i]
const start = cur.index + cur.matchLen
const end = (i+1<markers.length) ? markers[i+1].index : md.length
let text = md.slice(start, end)
text = text.replace(/[\u000c\r]+/g,'\n') // formfeed
text = text.replace(/\s+/g,' ').trim()
// Stop overly long spill
if(text.length>1500) text = text.slice(0,1500).trim()
entries.push({ ...cur, text })
}
// Aggregate into OT/NT JSON
const bookIndex = new Map(BOOKS.map(([n,_,t],i)=>[n,{testament:t, order:i+1}]))
const byBook = new Map()
for(const e of entries){
if(!byBook.has(e.book)) byBook.set(e.book, new Map())
const chMap = byBook.get(e.book)
if(!chMap.has(e.chapter)) chMap.set(e.chapter, new Map())
const vMap = chMap.get(e.chapter)
if(!vMap.has(e.verse)) vMap.set(e.verse, e.text)
}
const otBooks=[]; const ntBooks=[]
for(const [name, chMap] of byBook){
const meta = bookIndex.get(name)
const chapters=[]
for(const [ch, vMap] of Array.from(chMap.entries()).sort((a,b)=>a[0]-b[0])){
const verses=[]
for(const [vn, txt] of Array.from(vMap.entries()).sort((a,b)=>a[0]-b[0])){
verses.push({ verseNum: vn, text: txt })
}
if(verses.length>0) chapters.push({ chapterNum: ch, verses })
}
const bookObj={ name, chapters }
if(meta?.testament==='OT') otBooks.push({name,chapters})
else ntBooks.push({name,chapters})
}
// Sort books in canonical order
otBooks.sort((a,b)=>bookIndex.get(a.name).order-bookIndex.get(b.name).order)
ntBooks.sort((a,b)=>bookIndex.get(a.name).order-bookIndex.get(b.name).order)
const ot={ testament:'Old Testament', books: otBooks }
const nt={ testament:'New Testament', books: ntBooks }
const otFile = path.join(OUT_DIR,'old_testament.json')
const ntFile = path.join(OUT_DIR,'new_testament.json')
writeJson(otFile, ot)
writeJson(ntFile, nt)
console.log('Wrote:', otFile)
console.log('Wrote:', ntFile)
console.log('Books parsed:', otBooks.length + ntBooks.length)
}
main()

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env tsx
import fs from 'fs'
import path from 'path'
/*
Quick sample extractor from bibles/bible-bsb.md to our OT/NT JSON format.
- Looks for Genesis 3:2024 markers and builds a small sample JSON.
- Output directory: data/en_bible/BSB_SAMPLES
- Intended for demo/import testing without hitting API limits.
*/
const SRC = process.env.BSB_MD_PATH || path.join('bibles', 'bible-bsb.md')
const OUT = path.join('data', 'en_bible', 'BSB_SAMPLES')
function ensureDir(p: string) { fs.mkdirSync(p, { recursive: true }) }
function writeJson(file: string, obj: any) { ensureDir(path.dirname(file)); fs.writeFileSync(file, JSON.stringify(obj, null, 2), 'utf-8') }
function extractGenesis3Samples(md: string): { chapterNum: number; verses: { verseNum: number; text: string }[] } {
// Find all markers like "Genesis 3:20" and capture their file offsets
const regex = /Genesis\s+3:(\d+)/g
const indices: { verse: number; index: number }[] = []
for (const m of md.matchAll(regex) as any) {
const verse = parseInt(m[1], 10)
indices.push({ verse, index: m.index })
}
// We'll only keep verses 20..24 as a small sample
const keep = new Set([20, 21, 22, 23, 24])
const kept = indices.filter(x => keep.has(x.verse)).sort((a,b) => a.verse - b.verse)
const verses: { verseNum: number; text: string }[] = []
for (let i = 0; i < kept.length; i++) {
const cur = kept[i]
const next = kept[i+1]
const start = cur.index!
const end = next ? next.index! : Math.min(md.length, start + 2000) // cap window
let chunk = md.slice(start, end)
// Remove the marker itself and nearby page headers/footers and footnote junk
chunk = chunk.replace(/Genesis\s+3:\d+.*\n?/,'')
chunk = chunk.replace(/\f\d+\s*\|\s*Genesis\s*3:\d+.*\n?/g,'')
chunk = chunk.replace(/[\u000c\r]+/g,'\n') // form feed cleanup
chunk = chunk.replace(/\s+/g,' ').trim()
// Try to cut off before the next verse number embedded as an isolated number
const stop = chunk.search(/\s(?:2[1-9]|3\d|\d{1,2})\s/) // heuristic
const clean = (stop > 40 ? chunk.slice(0, stop) : chunk).trim()
if (clean.length > 0) verses.push({ verseNum: cur.verse, text: clean })
}
// Fallback if nothing captured
if (verses.length === 0) {
verses.push({ verseNum: 20, text: 'And Adam named his wife Eve, because she would be the mother of all the living.' })
}
return { chapterNum: 3, verses }
}
function main() {
if (!fs.existsSync(SRC)) {
console.error('Missing source file:', SRC)
process.exit(1)
}
const md = fs.readFileSync(SRC, 'utf-8')
const gen3 = extractGenesis3Samples(md)
const ot = {
testament: 'Old Testament',
books: [
{
name: 'Genesis',
chapters: [gen3]
}
]
}
// Minimal NT placeholder for structure completeness
const nt = { testament: 'New Testament', books: [] as any[] }
writeJson(path.join(OUT, 'old_testament.json'), ot)
writeJson(path.join(OUT, 'new_testament.json'), nt)
console.log('Wrote samples to', OUT)
}
main()

View File

@@ -0,0 +1,40 @@
import { PrismaClient } from '@prisma/client'
import path from 'path'
import fs from 'fs'
const prisma = new PrismaClient()
async function main() {
try {
const abbr = (process.env.EN_ABBR || 'WEB').toUpperCase()
const lang = 'en'
const dir = process.env.INPUT_DIR || path.join('data', 'en_bible', abbr)
if (!fs.existsSync(path.join(dir, 'old_testament.json')) || !fs.existsSync(path.join(dir, 'new_testament.json'))) {
console.error('Missing OT/NT JSON in', dir)
process.exit(1)
}
// Ensure version exists
let version = await prisma.bibleVersion.findUnique({ where: { abbreviation_language: { abbreviation: abbr, language: lang } } })
if (!version) {
version = await prisma.bibleVersion.create({ data: { name: abbr, abbreviation: abbr, language: lang, description: `English Bible (${abbr})`, isDefault: true } })
console.log('Created version', version.id)
} else {
// Make this the default and disable others
await prisma.bibleVersion.updateMany({ where: { language: lang }, data: { isDefault: false } })
await prisma.bibleVersion.update({ where: { id: version.id }, data: { isDefault: true } })
}
// Wipe current WEB content for a clean import
const delVerses = await prisma.bibleVerse.deleteMany({ where: { chapter: { book: { versionId: version.id } } } })
console.log('Deleted verses for', abbr, ':', delVerses.count)
} catch (e) {
console.error('Reset failed:', e)
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
main()

View File

@@ -0,0 +1,135 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const keyMap: Record<string, string> = {
// OT
'geneza': 'genesis',
'exodul': 'exodus',
'leviticul': 'leviticus',
'numerii': 'numbers',
'numeri': 'numbers',
'deuteronomul': 'deuteronomy',
'deuteronom': 'deuteronomy',
'iosua': 'joshua',
'judecătorii': 'judges',
'judecători': 'judges',
'judecatori': 'judges',
'rut': 'ruth',
'1 samuel': '1_samuel',
'2 samuel': '2_samuel',
'1 împăraţi': '1_kings',
'2 împăraţi': '2_kings',
'1 imparati': '1_kings',
'2 imparati': '2_kings',
'1 cronici': '1_chronicles',
'2 cronici': '2_chronicles',
'ezra': 'ezra',
'neemia': 'nehemiah',
'estera': 'esther',
'iov': 'job',
'psalmii': 'psalms',
'proverbele': 'proverbs',
'proverbe': 'proverbs',
'eclesiastul': 'ecclesiastes',
'ecclesiastul': 'ecclesiastes',
'cântarea cântărilor': 'song_of_songs',
'cantarea cantarilor': 'song_of_songs',
'isaia': 'isaiah',
'ieremia': 'jeremiah',
'plângerile': 'lamentations',
'plangerile': 'lamentations',
'plângerile lui ieremia': 'lamentations',
'ezechiel': 'ezekiel',
'daniel': 'daniel',
'osea': 'hosea',
'ioel': 'joel',
'amos': 'amos',
'obadia': 'obadiah',
'iona': 'jonah',
'mica': 'micah',
'naum': 'nahum',
'habacuc': 'habakkuk',
'ţefania': 'zephaniah',
'țefania': 'zephaniah',
'tefania': 'zephaniah',
'hagai': 'haggai',
'zaharia': 'zechariah',
'maleahi': 'malachi',
// NT
'matei': 'matthew',
'marcu': 'mark',
'luca': 'luke',
'ioan': 'john',
'faptele apostolilor': 'acts',
'romani': 'romans',
'1 corinteni': '1_corinthians',
'2 corinteni': '2_corinthians',
'galateni': 'galatians',
'efeseni': 'ephesians',
'filipeni': 'philippians',
'coloseni': 'colossians',
'1 tesaloniceni': '1_thessalonians',
'2 tesaloniceni': '2_thessalonians',
'1 timotei': '1_timothy',
'2 timotei': '2_timothy',
'tit': 'titus',
'titus': 'titus',
'filimon': 'philemon',
'evrei': 'hebrews',
'iacov': 'james',
'iacob': 'james',
'1 petru': '1_peter',
'2 petru': '2_peter',
'1 ioan': '1_john',
'2 ioan': '2_john',
'3 ioan': '3_john',
'iuda': 'jude',
'apocalipsa': 'revelation',
'revelaţia': 'revelation',
'revelația': 'revelation',
}
function toCanonicalBookKey(name: string): string {
const k = name.trim().toLowerCase()
return keyMap[k] || k.replace(/\s+/g, '_')
}
async function main() {
try {
const roVersion = await prisma.bibleVersion.findFirst({
where: { language: { in: ['ro', 'RO'] } }
})
if (!roVersion) {
throw new Error('No Romanian BibleVersion found (language ro)')
}
const books = await prisma.bibleBook.findMany({
where: { versionId: roVersion.id },
orderBy: { orderNum: 'asc' }
})
let updated = 0
for (const b of books) {
const desiredKey = toCanonicalBookKey(b.name)
if (b.bookKey !== desiredKey) {
await prisma.bibleBook.update({
where: { id: b.id },
data: { bookKey: desiredKey }
})
updated++
console.log(`Updated ${b.name}: ${b.bookKey} -> ${desiredKey}`)
}
}
console.log(`Resync complete. Updated ${updated} book keys for RO.`)
} catch (err) {
console.error('Resync failed:', err)
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
main()

View File

@@ -0,0 +1,94 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
console.log('Seeding prayer requests...')
const prayers = [
{
title: 'Rugăciune pentru vindecare',
description: 'Te rog să te rogi pentru tatăl meu care se află în spital. Are nevoie de vindecarea lui Dumnezeu și de putere pentru a trece prin această perioadă dificilă.',
category: 'health',
author: 'Maria P.',
isAnonymous: false,
prayerCount: 23
},
{
title: 'Îndrumarea lui Dumnezeu în carieră',
description: 'Caut direcția lui Dumnezeu pentru următorul pas în cariera mea. Te rog să te rogi pentru claritate și pace în luarea acestei decizii importante.',
category: 'work',
author: 'Alexandru M.',
isAnonymous: false,
prayerCount: 15
},
{
title: 'Unitatea în familia noastră',
description: 'Rugați-vă pentru restaurarea relațiilor în familia noastră și pentru iertarea reciprocă. Avem nevoie de vindecarea rănilor din trecut.',
category: 'family',
author: 'Anonim',
isAnonymous: true,
prayerCount: 41
},
{
title: 'Pentru misionarii din Africa',
description: 'Rugați-vă pentru protecția și proviziunea pentru misionarii noștri care lucrează în Africa de Vest, în special pentru familia Popescu.',
category: 'ministry',
author: 'Pavel R.',
isAnonymous: false,
prayerCount: 12
},
{
title: 'Pace în Ucraina',
description: 'Să ne rugăm pentru pace și protecție pentru poporul ucrainean în aceste timpuri dificile. Pentru familiile despărțite și pentru cei care suferă.',
category: 'world',
author: 'Comunitatea',
isAnonymous: false,
prayerCount: 89
},
{
title: 'Trecerea prin depresie',
description: 'Am nevoie de rugăciuni pentru a trece prin această perioadă grea de depresie și anxietate. Cred că Dumnezeu poate să mă vindece.',
category: 'personal',
author: 'Anonim',
isAnonymous: true,
prayerCount: 34
},
{
title: 'Protecție pentru copiii noștri',
description: 'Rugați-vă pentru protecția copiilor noștri la școală și pentru înțelepciune în creșterea lor în credință.',
category: 'family',
author: 'Elena și Mihai',
isAnonymous: false,
prayerCount: 28
},
{
title: 'Vindecare de cancer',
description: 'Sora mea a fost diagnosticată cu cancer. Credem în puterea vindecătoare a lui Dumnezeu și avem nevoie de susținerea voastră în rugăciune.',
category: 'health',
author: 'Andreea S.',
isAnonymous: false,
prayerCount: 67
}
]
for (const prayer of prayers) {
await prisma.prayerRequest.create({
data: {
...prayer,
isActive: true
}
})
}
console.log('Prayer requests seeded successfully!')
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

141
scripts/old/test-prayers.ts Normal file
View File

@@ -0,0 +1,141 @@
const BASE_URL = 'http://localhost:3010'
async function testPrayerAPI() {
console.log('🧪 Testing Prayer API Endpoints...\n')
// Test 1: Get all prayers
console.log('📋 Test 1: Fetching all prayers...')
try {
const response = await fetch(`${BASE_URL}/api/prayers?limit=10`)
const data = await response.json()
console.log(`✅ Success: Retrieved ${data.prayers?.length || 0} prayers`)
console.log(` First prayer: ${data.prayers?.[0]?.title || 'N/A'}\n`)
} catch (error) {
console.log(`❌ Error fetching prayers: ${error}\n`)
}
// Test 2: Get prayers by category
console.log('📋 Test 2: Fetching prayers by category (health)...')
try {
const response = await fetch(`${BASE_URL}/api/prayers?category=health&limit=5`)
const data = await response.json()
console.log(`✅ Success: Retrieved ${data.prayers?.length || 0} health prayers\n`)
} catch (error) {
console.log(`❌ Error fetching category prayers: ${error}\n`)
}
// Test 3: Create a new prayer
console.log('📋 Test 3: Creating a new prayer request...')
try {
const newPrayer = {
title: 'Test Prayer Request',
description: 'This is a test prayer request created by the testing script.',
category: 'personal',
isAnonymous: false
}
const response = await fetch(`${BASE_URL}/api/prayers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPrayer)
})
const data = await response.json()
if (response.ok) {
console.log(`✅ Success: Created prayer with ID: ${data.prayer?.id}`)
console.log(` Title: ${data.prayer?.title}\n`)
return data.prayer?.id // Return ID for next test
} else {
console.log(`❌ Error: ${data.error}\n`)
}
} catch (error) {
console.log(`❌ Error creating prayer: ${error}\n`)
}
// Test 4: Update prayer count (pray for a prayer)
console.log('📋 Test 4: Testing prayer count update...')
try {
// Get first prayer ID
const getResponse = await fetch(`${BASE_URL}/api/prayers?limit=1`)
const getData = await getResponse.json()
const prayerId = getData.prayers?.[0]?.id
if (prayerId) {
const response = await fetch(`${BASE_URL}/api/prayers/${prayerId}/pray`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
const data = await response.json()
if (response.ok) {
console.log(`✅ Success: Updated prayer count`)
console.log(` New count: ${data.prayerCount}\n`)
} else {
console.log(`❌ Error: ${data.error}\n`)
}
} else {
console.log('❌ No prayer found to test with\n')
}
} catch (error) {
console.log(`❌ Error updating prayer count: ${error}\n`)
}
// Test 5: Generate AI prayer
console.log('📋 Test 5: Testing AI prayer generation...')
try {
const aiRequest = {
prompt: 'I need strength to overcome my anxiety about the future',
category: 'personal',
locale: 'en'
}
const response = await fetch(`${BASE_URL}/api/prayers/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(aiRequest)
})
const data = await response.json()
if (response.ok) {
console.log(`✅ Success: Generated AI prayer`)
console.log(` Title: ${data.title}`)
console.log(` Prayer preview: ${data.prayer?.substring(0, 100)}...\n`)
} else {
console.log(`❌ Error: ${data.error}\n`)
}
} catch (error) {
console.log(`❌ Error generating AI prayer: ${error}\n`)
}
// Test 6: Generate Romanian AI prayer
console.log('📋 Test 6: Testing Romanian AI prayer generation...')
try {
const aiRequest = {
prompt: 'Am nevoie de înțelepciune pentru o decizie importantă',
category: 'personal',
locale: 'ro'
}
const response = await fetch(`${BASE_URL}/api/prayers/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(aiRequest)
})
const data = await response.json()
if (response.ok) {
console.log(`✅ Success: Generated Romanian AI prayer`)
console.log(` Title: ${data.title}`)
console.log(` Prayer preview: ${data.prayer?.substring(0, 100)}...\n`)
} else {
console.log(`❌ Error: ${data.error}\n`)
}
} catch (error) {
console.log(`❌ Error generating Romanian AI prayer: ${error}\n`)
}
console.log('✨ Prayer API testing completed!')
}
// Run tests
testPrayerAPI().catch(console.error)

173
scripts/old/usfm-to-json.ts Normal file
View File

@@ -0,0 +1,173 @@
#!/usr/bin/env tsx
import fs from 'fs'
import path from 'path'
/*
Convert a directory of USFM files (e.g., WEB/KJV) into our OT/NT JSON format.
Env:
- INPUT_USFM_DIR: path to folder with *.usfm files (unzipped)
- EN_ABBR: English version abbreviation for output folder (e.g., WEB or KJV)
- OUTPUT_DIR (optional): defaults to data/en_bible/<EN_ABBR>
Output:
- <OUTPUT_DIR>/old_testament.json
- <OUTPUT_DIR>/new_testament.json
USFM markers parsed:
- \id <BOOKID>
- \h <Header/Book name> (optional)
- \c <chapter number>
- \v <verse number> <text>
*/
const INPUT = process.env.INPUT_USFM_DIR || ''
const ABBR = (process.env.EN_ABBR || 'WEB').toUpperCase()
const OUTPUT_DIR = process.env.OUTPUT_DIR || path.join('data','en_bible', ABBR)
if (!INPUT || !fs.existsSync(INPUT)) {
console.error('Missing or invalid INPUT_USFM_DIR. Set INPUT_USFM_DIR to a folder containing *.usfm files (unzipped).')
process.exit(1)
}
function ensureDir(p: string) { fs.mkdirSync(p, { recursive: true }) }
function writeJson(file: string, obj: any) { ensureDir(path.dirname(file)); fs.writeFileSync(file, JSON.stringify(obj, null, 2), 'utf-8') }
// Canonical order + mapping from USFM book codes to English names + testament
// Based on standard Protestant canon 66 books
type CanonEntry = { code: string; name: string; testament: 'OT'|'NT' }
const CANON: CanonEntry[] = [
{code:'GEN',name:'Genesis',testament:'OT'},{code:'EXO',name:'Exodus',testament:'OT'},{code:'LEV',name:'Leviticus',testament:'OT'},
{code:'NUM',name:'Numbers',testament:'OT'},{code:'DEU',name:'Deuteronomy',testament:'OT'},{code:'JOS',name:'Joshua',testament:'OT'},
{code:'JDG',name:'Judges',testament:'OT'},{code:'RUT',name:'Ruth',testament:'OT'},{code:'1SA',name:'1 Samuel',testament:'OT'},
{code:'2SA',name:'2 Samuel',testament:'OT'},{code:'1KI',name:'1 Kings',testament:'OT'},{code:'2KI',name:'2 Kings',testament:'OT'},
{code:'1CH',name:'1 Chronicles',testament:'OT'},{code:'2CH',name:'2 Chronicles',testament:'OT'},{code:'EZR',name:'Ezra',testament:'OT'},
{code:'NEH',name:'Nehemiah',testament:'OT'},{code:'EST',name:'Esther',testament:'OT'},{code:'JOB',name:'Job',testament:'OT'},
{code:'PSA',name:'Psalms',testament:'OT'},{code:'PRO',name:'Proverbs',testament:'OT'},{code:'ECC',name:'Ecclesiastes',testament:'OT'},
{code:'SNG',name:'Song of Songs',testament:'OT'},{code:'ISA',name:'Isaiah',testament:'OT'},{code:'JER',name:'Jeremiah',testament:'OT'},
{code:'LAM',name:'Lamentations',testament:'OT'},{code:'EZK',name:'Ezekiel',testament:'OT'},{code:'DAN',name:'Daniel',testament:'OT'},
{code:'HOS',name:'Hosea',testament:'OT'},{code:'JOL',name:'Joel',testament:'OT'},{code:'AMO',name:'Amos',testament:'OT'},
{code:'OBA',name:'Obadiah',testament:'OT'},{code:'JON',name:'Jonah',testament:'OT'},{code:'MIC',name:'Micah',testament:'OT'},
{code:'NAM',name:'Nahum',testament:'OT'},{code:'HAB',name:'Habakkuk',testament:'OT'},{code:'ZEP',name:'Zephaniah',testament:'OT'},
{code:'HAG',name:'Haggai',testament:'OT'},{code:'ZEC',name:'Zechariah',testament:'OT'},{code:'MAL',name:'Malachi',testament:'OT'},
{code:'MAT',name:'Matthew',testament:'NT'},{code:'MRK',name:'Mark',testament:'NT'},{code:'LUK',name:'Luke',testament:'NT'},
{code:'JHN',name:'John',testament:'NT'},{code:'ACT',name:'Acts',testament:'NT'},{code:'ROM',name:'Romans',testament:'NT'},
{code:'1CO',name:'1 Corinthians',testament:'NT'},{code:'2CO',name:'2 Corinthians',testament:'NT'},{code:'GAL',name:'Galatians',testament:'NT'},
{code:'EPH',name:'Ephesians',testament:'NT'},{code:'PHP',name:'Philippians',testament:'NT'},{code:'COL',name:'Colossians',testament:'NT'},
{code:'1TH',name:'1 Thessalonians',testament:'NT'},{code:'2TH',name:'2 Thessalonians',testament:'NT'},{code:'1TI',name:'1 Timothy',testament:'NT'},
{code:'2TI',name:'2 Timothy',testament:'NT'},{code:'TIT',name:'Titus',testament:'NT'},{code:'PHM',name:'Philemon',testament:'NT'},
{code:'HEB',name:'Hebrews',testament:'NT'},{code:'JAS',name:'James',testament:'NT'},{code:'1PE',name:'1 Peter',testament:'NT'},
{code:'2PE',name:'2 Peter',testament:'NT'},{code:'1JN',name:'1 John',testament:'NT'},{code:'2JN',name:'2 John',testament:'NT'},
{code:'3JN',name:'3 John',testament:'NT'},{code:'JUD',name:'Jude',testament:'NT'},{code:'REV',name:'Revelation',testament:'NT'}
]
const CODE_TO_META = new Map(CANON.map((c,i)=>[c.code,{...c, order:i+1}]))
type Verse = { verseNum:number; text:string }
type Chapter = { chapterNum:number; verses:Verse[] }
type Book = { name:string; code:string; testament:'OT'|'NT'; chapters:Chapter[] }
function parseUsfmFile(file: string): Book | null {
const lines = fs.readFileSync(file,'utf-8').split(/\r?\n/)
let code = ''
let name = ''
let currentChapter = 0
let currentVerses: Verse[] = []
const chapters = new Map<number, Verse[]>()
for (let raw of lines) {
const line = raw.trim()
if (/^\\id\s+/.test(line)) {
const m = line.match(/^\\id\s+(\S+)/)
if (m) code = m[1].toUpperCase()
continue
}
if (/^\\h\s+/.test(line)) {
// \h Genesis
name = line.replace(/^\\h\s+/, '').trim()
continue
}
if (/^\\c\s+/.test(line)) {
// new chapter
if (currentChapter > 0) chapters.set(currentChapter, currentVerses)
currentChapter = parseInt(line.slice(3).trim(), 10)
currentVerses = []
continue
}
if (/^\\v\s+/.test(line)) {
// \v 1 In the beginning God...
const m = line.match(/^\\v\s+(\d+)\s+(.*)$/)
if (m) {
const verseNum = parseInt(m[1], 10)
let text = m[2]
// Strip inline USFM markup, preserving words
// Remove word wrappers: \w Word|strong="..."\w* and \+w ... \+w*
text = text.replace(/\\\+?w\s+/gi, '')
.replace(/\|strong="[^"]*"/gi, '')
.replace(/\\\+?w\*/gi, '')
// Remove footnotes / cross-refs blocks: \f ... \f* and \x ... \x*
text = text.replace(/\\f\s+.*?\\f\*/gis, ' ')
.replace(/\\x\s+.*?\\x\*/gis, ' ')
// Remove any remaining inline tags like \\add, \\nd, \\qs, etc.
text = text.replace(/\\[a-z0-9-]+\s*/gi, ' ')
// Collapse whitespace
text = text.replace(/\s+/g, ' ').trim()
currentVerses.push({ verseNum, text })
}
continue
}
// Some USFM wrap text on subsequent lines; append to last verse if applicable
if (currentVerses.length > 0 && line && !line.startsWith('\\')) {
const last = currentVerses[currentVerses.length - 1]
last.text = (last.text + ' ' + line).replace(/\s+/g,' ').trim()
}
}
if (currentChapter > 0) chapters.set(currentChapter, currentVerses)
// Resolve name/code/testament
const meta = CODE_TO_META.get(code)
if (!meta) return null
const finalName = name || meta.name
const book: Book = { name: finalName, code, testament: meta.testament, chapters: [] }
for (const [ch, verses] of Array.from(chapters.entries()).sort((a,b)=>a[0]-b[0])) {
if (verses.length > 0) book.chapters.push({ chapterNum: ch, verses })
}
return book
}
function main() {
const files = fs.readdirSync(INPUT).filter(f=>f.toLowerCase().endsWith('.usfm'))
console.log('USFM files found:', files.length)
if (files.length === 0) {
console.error('No .usfm files found in', INPUT)
process.exit(1)
}
const books: Book[] = []
for (const f of files) {
const full = path.join(INPUT, f)
const b = parseUsfmFile(full)
if (b && b.chapters.length > 0) {
books.push(b)
} else {
// basic debug
// console.log('Skipping', f, 'parsed:', !!b, 'chapters:', b?.chapters.length)
}
}
// Partition
const otBooks = books.filter(b => b.testament === 'OT').sort((a,b)=>CODE_TO_META.get(a.code)!.order - CODE_TO_META.get(b.code)!.order)
const ntBooks = books.filter(b => b.testament === 'NT').sort((a,b)=>CODE_TO_META.get(a.code)!.order - CODE_TO_META.get(b.code)!.order)
const ot = { testament: 'Old Testament', books: otBooks.map(b=>({ name:b.name, chapters:b.chapters })) }
const nt = { testament: 'New Testament', books: ntBooks.map(b=>({ name:b.name, chapters:b.chapters })) }
const otFile = path.join(OUTPUT_DIR, 'old_testament.json')
const ntFile = path.join(OUTPUT_DIR, 'new_testament.json')
writeJson(otFile, ot)
writeJson(ntFile, nt)
console.log('Wrote:', otFile)
console.log('Wrote:', ntFile)
console.log('Books:', books.length, 'OT:', otBooks.length, 'NT:', ntBooks.length)
}
main()

View File

@@ -0,0 +1,143 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const SRC = process.env.BSB_MD_PATH || path.join('bibles', 'bible-bsb.md')
function canon() {
const OT = [
['Genesis', ['Genesis'], 50],
['Exodus', ['Exodus'], 40],
['Leviticus', ['Leviticus'], 27],
['Numbers', ['Numbers'], 36],
['Deuteronomy', ['Deuteronomy'], 34],
['Joshua', ['Joshua'], 24],
['Judges', ['Judges'], 21],
['Ruth', ['Ruth'], 4],
['1 Samuel', ['1 Samuel','1 Samuel'], 31],
['2 Samuel', ['2 Samuel','2 Samuel'], 24],
['1 Kings', ['1 Kings','1 Kings'], 22],
['2 Kings', ['2 Kings','2 Kings'], 25],
['1 Chronicles', ['1 Chronicles','1 Chronicles'], 29],
['2 Chronicles', ['2 Chronicles','2 Chronicles'], 36],
['Ezra', ['Ezra'], 10],
['Nehemiah', ['Nehemiah'], 13],
['Esther', ['Esther'], 10],
['Job', ['Job'], 42],
['Psalms', ['Psalms','Psalm'], 150],
['Proverbs', ['Proverbs'], 31],
['Ecclesiastes', ['Ecclesiastes'], 12],
['Song of Songs', ['Song of Songs','Song of Solomon'], 8],
['Isaiah', ['Isaiah'], 66],
['Jeremiah', ['Jeremiah'], 52],
['Lamentations', ['Lamentations'], 5],
['Ezekiel', ['Ezekiel'], 48],
['Daniel', ['Daniel'], 12],
['Hosea', ['Hosea'], 14],
['Joel', ['Joel'], 3],
['Amos', ['Amos'], 9],
['Obadiah', ['Obadiah'], 1],
['Jonah', ['Jonah'], 4],
['Micah', ['Micah'], 7],
['Nahum', ['Nahum'], 3],
['Habakkuk', ['Habakkuk'], 3],
['Zephaniah', ['Zephaniah'], 3],
['Haggai', ['Haggai'], 2],
['Zechariah', ['Zechariah'], 14],
['Malachi', ['Malachi'], 4]
]
const NT = [
['Matthew', ['Matthew'], 28],
['Mark', ['Mark'], 16],
['Luke', ['Luke'], 24],
['John', ['John'], 21],
['Acts', ['Acts'], 28],
['Romans', ['Romans'], 16],
['1 Corinthians', ['1 Corinthians','1 Corinthians'], 16],
['2 Corinthians', ['2 Corinthians','2 Corinthians'], 13],
['Galatians', ['Galatians'], 6],
['Ephesians', ['Ephesians'], 6],
['Philippians', ['Philippians'], 4],
['Colossians', ['Colossians'], 4],
['1 Thessalonians', ['1 Thessalonians','1 Thessalonians'], 5],
['2 Thessalonians', ['2 Thessalonians','2 Thessalonians'], 3],
['1 Timothy', ['1 Timothy','1 Timothy'], 6],
['2 Timothy', ['2 Timothy','2 Timothy'], 4],
['Titus', ['Titus'], 3],
['Philemon', ['Philemon'], 1],
['Hebrews', ['Hebrews'], 13],
['James', ['James'], 5],
['1 Peter', ['1 Peter','1 Peter'], 5],
['2 Peter', ['2 Peter','2 Peter'], 3],
['1 John', ['1 John','1 John'], 5],
['2 John', ['2 John','2 John'], 1],
['3 John', ['3 John','3 John'], 1],
['Jude', ['Jude'], 1],
['Revelation', ['Revelation'], 22]
]
return [
...OT.map(([n,v,c]) => ({ name:n, variants:v, expectedChapters:c, testament:'OT' })),
...NT.map(([n,v,c]) => ({ name:n, variants:v, expectedChapters:c, testament:'NT' })),
]
}
function main() {
if (!fs.existsSync(SRC)) {
console.error('Missing source file:', SRC)
process.exit(1)
}
const md = fs.readFileSync(SRC, 'utf-8')
const books = canon()
const report = { file: SRC, totals: { versesTagged: 0 }, books: [] }
for (const b of books) {
const patterns = b.variants.map(v => v.replace(/\s+/g, '\\s+'))
const names = patterns.join('|')
const re = new RegExp(`(?:^|[\n\r\f\s\|\(])(?:${names})\\s+(\\d+):(\\d+)`, 'gi')
const chapters = new Set()
let m
let verseCount = 0
while ((m = re.exec(md)) !== null) {
const nums = m.slice(1).filter(Boolean)
const ch = parseInt(nums[0] || '0', 10)
const vs = parseInt(nums[1] || '0', 10)
if (Number.isFinite(ch) && ch > 0) chapters.add(ch)
if (Number.isFinite(vs) && vs > 0) verseCount++
}
// Heuristic: some one-chapter books may lack inline verse references; accept header presence
const oneChapterBooks = new Set(['Obadiah','Philemon','2 John','3 John','Jude'])
if (chapters.size === 0 && oneChapterBooks.has(b.name)) {
const headerRe = new RegExp(`[\f\n\r]\s*${b.variants.map(v=>v.replace(/\s+/g,'\\s+')).join('|')}\s*[\n\r]`, 'i')
if (headerRe.test(md)) {
chapters.add(1)
}
}
report.totals.versesTagged += verseCount
report.books.push({
name: b.name,
testament: b.testament,
expectedChapters: b.expectedChapters,
detectedChapters: Array.from(chapters).sort((a,b)=>a-b),
detectedCount: chapters.size,
coverage: b.expectedChapters > 0 ? +(100 * chapters.size / b.expectedChapters).toFixed(2) : null,
verseMarkers: verseCount
})
}
const missingBooks = report.books.filter(x => x.detectedCount === 0).map(x=>x.name)
const partialBooks = report.books.filter(x => x.detectedCount > 0 && x.detectedCount < x.expectedChapters).map(x=>({name:x.name, det:x.detectedCount, exp:x.expectedChapters}))
console.log('Validation summary for', SRC)
console.log('Total verse markers found:', report.totals.versesTagged)
console.log('Books missing markers:', missingBooks.length ? missingBooks.join(', ') : 'None')
console.log('Books partially detected (chapters):', partialBooks.length ? JSON.stringify(partialBooks.slice(0,10)) : 'None')
const outDir = path.join('data','en_bible','BSB_VALIDATION')
fs.mkdirSync(outDir, { recursive: true })
fs.writeFileSync(path.join(outDir,'report.json'), JSON.stringify(report, null, 2), 'utf-8')
console.log('Wrote detailed report to', path.join(outDir,'report.json'))
}
main()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env tsx
import fs from 'fs'
import path from 'path'
const SRC = process.env.BSB_MD_PATH || path.join('bibles', 'bible-bsb.md')
type BookInfo = { name: string; variants: string[]; expectedChapters: number; testament: 'OT'|'NT' }
function canon(): BookInfo[] {
const OT: Array<[string, string[], number]> = [
['Genesis', ['Genesis'], 50],
['Exodus', ['Exodus'], 40],
['Leviticus', ['Leviticus'], 27],
['Numbers', ['Numbers'], 36],
['Deuteronomy', ['Deuteronomy'], 34],
['Joshua', ['Joshua'], 24],
['Judges', ['Judges'], 21],
['Ruth', ['Ruth'], 4],
['1 Samuel', ['1 Samuel','1 Samuel'], 31],
['2 Samuel', ['2 Samuel','2 Samuel'], 24],
['1 Kings', ['1 Kings','1 Kings'], 22],
['2 Kings', ['2 Kings','2 Kings'], 25],
['1 Chronicles', ['1 Chronicles','1 Chronicles'], 29],
['2 Chronicles', ['2 Chronicles','2 Chronicles','2 Chronicles'], 36],
['Ezra', ['Ezra'], 10],
['Nehemiah', ['Nehemiah'], 13],
['Esther', ['Esther'], 10],
['Job', ['Job'], 42],
['Psalms', ['Psalms','Psalm'], 150],
['Proverbs', ['Proverbs'], 31],
['Ecclesiastes', ['Ecclesiastes'], 12],
['Song of Songs', ['Song of Songs','Song of Solomon'], 8],
['Isaiah', ['Isaiah'], 66],
['Jeremiah', ['Jeremiah'], 52],
['Lamentations', ['Lamentations'], 5],
['Ezekiel', ['Ezekiel'], 48],
['Daniel', ['Daniel'], 12],
['Hosea', ['Hosea'], 14],
['Joel', ['Joel'], 3],
['Amos', ['Amos'], 9],
['Obadiah', ['Obadiah'], 1],
['Jonah', ['Jonah'], 4],
['Micah', ['Micah'], 7],
['Nahum', ['Nahum'], 3],
['Habakkuk', ['Habakkuk'], 3],
['Zephaniah', ['Zephaniah'], 3],
['Haggai', ['Haggai'], 2],
['Zechariah', ['Zechariah'], 14],
['Malachi', ['Malachi'], 4]
]
const NT: Array<[string, string[], number]> = [
['Matthew', ['Matthew'], 28],
['Mark', ['Mark'], 16],
['Luke', ['Luke'], 24],
['John', ['John'], 21],
['Acts', ['Acts'], 28],
['Romans', ['Romans'], 16],
['1 Corinthians', ['1 Corinthians','1 Corinthians'], 16],
['2 Corinthians', ['2 Corinthians','2 Corinthians'], 13],
['Galatians', ['Galatians'], 6],
['Ephesians', ['Ephesians'], 6],
['Philippians', ['Philippians'], 4],
['Colossians', ['Colossians'], 4],
['1 Thessalonians', ['1 Thessalonians','1 Thessalonians'], 5],
['2 Thessalonians', ['2 Thessalonians','2 Thessalonians'], 3],
['1 Timothy', ['1 Timothy','1 Timothy'], 6],
['2 Timothy', ['2 Timothy','2 Timothy'], 4],
['Titus', ['Titus'], 3],
['Philemon', ['Philemon'], 1],
['Hebrews', ['Hebrews'], 13],
['James', ['James'], 5],
['1 Peter', ['1 Peter','1 Peter'], 5],
['2 Peter', ['2 Peter','2 Peter'], 3],
['1 John', ['1 John','1 John'], 5],
['2 John', ['2 John','2 John'], 1],
['3 John', ['3 John','3 John'], 1],
['Jude', ['Jude'], 1],
['Revelation', ['Revelation'], 22]
]
return [
...OT.map(([n,v,c]) => ({ name:n, variants:v, expectedChapters:c, testament:'OT' as const })),
...NT.map(([n,v,c]) => ({ name:n, variants:v, expectedChapters:c, testament:'NT' as const })),
]
}
function escapeRegExp(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function main() {
if (!fs.existsSync(SRC)) {
console.error('Missing source file:', SRC)
process.exit(1)
}
const md = fs.readFileSync(SRC, 'utf-8')
const books = canon()
const report: any = { file: SRC, totals: { versesTagged: 0 }, books: [] as any[] }
for (const b of books) {
// Build a regex to find markers like: "... | BookName 12:34" or just "BookName 12:34"
// Allow flexible whitespace and the double-spaced variants in source.
const patterns = b.variants.map(v => v.replace(/\s+/g, '\\s+'))
const combined = patterns.map(p => `(?:^|[\n\r\f\s\|])${p}\\s+(\\d+):(\\d+)`).join('|')
const re = new RegExp(combined, 'gi')
const chapters = new Set<number>()
let m: RegExpExecArray | null
let verseCount = 0
while ((m = re.exec(md)) !== null) {
// Find first numeric capture among alternations
const nums = m.slice(1).filter(Boolean)
const ch = parseInt(nums[0] || '0', 10)
const vs = parseInt(nums[1] || '0', 10)
if (Number.isFinite(ch) && ch > 0) chapters.add(ch)
if (Number.isFinite(vs) && vs > 0) verseCount++
}
report.totals.versesTagged += verseCount
report.books.push({
name: b.name,
testament: b.testament,
expectedChapters: b.expectedChapters,
detectedChapters: [...chapters].sort((a,b)=>a-b),
detectedCount: chapters.size,
coverage: b.expectedChapters > 0 ? +(100 * chapters.size / b.expectedChapters).toFixed(2) : null,
verseMarkers: verseCount
})
}
const missingBooks = report.books.filter((x:any) => x.detectedCount === 0).map((x:any)=>x.name)
const partialBooks = report.books.filter((x:any) => x.detectedCount > 0 && x.detectedCount < x.expectedChapters).map((x:any)=>({name:x.name, det:x.detectedCount, exp:x.expectedChapters}))
console.log('Validation summary for', SRC)
console.log('Total verse markers found:', report.totals.versesTagged)
console.log('Books missing markers:', missingBooks.length ? missingBooks.join(', ') : 'None')
console.log('Books partially detected (chapters):', partialBooks.length ? partialBooks.slice(0,10) : 'None')
const outDir = path.join('data','en_bible','BSB_VALIDATION')
fs.mkdirSync(outDir, { recursive: true })
fs.writeFileSync(path.join(outDir,'report.json'), JSON.stringify(report, null, 2), 'utf-8')
console.log('Wrote detailed report to', path.join(outDir,'report.json'))
}
main()