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:
48
scripts/old/check-admin.ts
Normal file
48
scripts/old/check-admin.ts
Normal 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();
|
||||
47
scripts/old/clean-json-text.js
Normal file
47
scripts/old/clean-json-text.js
Normal 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');
|
||||
63
scripts/old/cleanup-english-versions.ts
Normal file
63
scripts/old/cleanup-english-versions.ts
Normal 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()
|
||||
|
||||
74
scripts/old/clone_vector_table.ts
Normal file
74
scripts/old/clone_vector_table.ts
Normal 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()
|
||||
227
scripts/old/convert-fidela-md-to-json.ts
Normal file
227
scripts/old/convert-fidela-md-to-json.ts
Normal 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)
|
||||
})
|
||||
175
scripts/old/fetch-english-bible.ts
Normal file
175
scripts/old/fetch-english-bible.ts
Normal 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)
|
||||
})
|
||||
|
||||
281
scripts/old/import-api-bible.ts
Normal file
281
scripts/old/import-api-bible.ts
Normal 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
184
scripts/old/import-bible.ts
Normal 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())
|
||||
140
scripts/old/import-english-json.ts
Normal file
140
scripts/old/import-english-json.ts
Normal 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()
|
||||
|
||||
286
scripts/old/import-english-versioned.ts
Normal file
286
scripts/old/import-english-versioned.ts
Normal 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())
|
||||
305
scripts/old/import-romanian-bible-md.ts
Normal file
305
scripts/old/import-romanian-bible-md.ts
Normal 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 }
|
||||
378
scripts/old/import-romanian-bible.ts
Normal file
378
scripts/old/import-romanian-bible.ts
Normal 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())
|
||||
230
scripts/old/import-romanian-versioned.ts
Normal file
230
scripts/old/import-romanian-versioned.ts
Normal 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
67
scripts/old/init.sql
Normal 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;
|
||||
187
scripts/old/migrate-to-versioned-schema.ts
Normal file
187
scripts/old/migrate-to-versioned-schema.ts
Normal 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
140
scripts/old/optimize-db.sql
Normal 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;
|
||||
103
scripts/old/parse-bsb-md-full.js
Normal file
103
scripts/old/parse-bsb-md-full.js
Normal 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()
|
||||
83
scripts/old/parse-bsb-md-samples.ts
Normal file
83
scripts/old/parse-bsb-md-samples.ts
Normal 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:20–24 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()
|
||||
|
||||
40
scripts/old/reset-web-version.ts
Normal file
40
scripts/old/reset-web-version.ts
Normal 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()
|
||||
135
scripts/old/resync-bookkeys-ro.ts
Normal file
135
scripts/old/resync-bookkeys-ro.ts
Normal 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()
|
||||
|
||||
94
scripts/old/seed-prayers.ts
Normal file
94
scripts/old/seed-prayers.ts
Normal 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
141
scripts/old/test-prayers.ts
Normal 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
173
scripts/old/usfm-to-json.ts
Normal 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()
|
||||
143
scripts/old/validate-bsb-md.js
Normal file
143
scripts/old/validate-bsb-md.js
Normal 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()
|
||||
145
scripts/old/validate-bsb-md.ts
Normal file
145
scripts/old/validate-bsb-md.ts
Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user