Fix Edge-incompatible middleware; set Node runtime on Prisma/pg routes; add full Romanian Bible import + converter; import data JSON; resync RO bookKeys; stabilize /api/bible/books locale fallback; restart dev server.

This commit is contained in:
andupetcu
2025-09-20 18:01:04 +03:00
parent 500066450d
commit 88b251c100
28 changed files with 127926 additions and 175 deletions

View File

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

View File

@@ -123,7 +123,7 @@ async function importFromApiBible() {
let totalVersesImported = 0
for (const book of books.slice(0, 2)) { // Limit to first 2 books for sample structure
for (const book of books.slice(0, 10)) { // Import first 10 books
console.log(`Processing ${book.name} (${book.id})...`)
const orderNum = getBookOrderNumber(book.id)
@@ -150,7 +150,7 @@ async function importFromApiBible() {
console.log(` Found ${chapters.length} chapters`)
for (const chapter of chapters.slice(0, 2)) { // Limit to first 2 chapters for sample
for (const chapter of chapters.slice(0, 5)) { // Import first 5 chapters
const chapterNum = parseChapterNumber(chapter.id)
console.log(` Processing chapter ${chapterNum}...`)
@@ -175,10 +175,10 @@ async function importFromApiBible() {
console.log(` Found ${versesResponse.data.length} verses`)
// Process only first 5 verses for sample structure
const sampleVerses = versesResponse.data.slice(0, 5)
for (let i = 0; i < sampleVerses.length; i += 5) {
const verseBatch = sampleVerses.slice(i, i + 5)
// 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 {

View File

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

View File

@@ -0,0 +1,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())

View File

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

View File

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