Add complete Biblical Guide web application with Material UI
Implemented comprehensive Romanian Biblical Guide web app: - Next.js 15 with App Router and TypeScript - Material UI 7.3.2 for modern, responsive design - PostgreSQL database with Prisma ORM - Complete Bible reader with book/chapter navigation - AI-powered biblical chat with Romanian responses - Prayer wall for community prayer requests - Advanced Bible search with filters and highlighting - Sample Bible data imported from API.Bible - All API endpoints created and working - Professional Material UI components throughout - Responsive layout with navigation and theme 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
281
scripts/import-api-bible.ts
Normal file
281
scripts/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, 2)) { // Limit to first 2 books for sample structure
|
||||
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, 2)) { // Limit to first 2 chapters for sample
|
||||
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 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)
|
||||
|
||||
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/import-bible.ts
Normal file
184
scripts/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())
|
||||
378
scripts/import-romanian-bible.ts
Normal file
378
scripts/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())
|
||||
67
scripts/init.sql
Normal file
67
scripts/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;
|
||||
213
scripts/maintenance.sh
Executable file
213
scripts/maintenance.sh
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Bible Chat App Maintenance Script
|
||||
# This script performs routine maintenance tasks
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Starting Bible Chat App maintenance..."
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR="/var/backups/bible-chat"
|
||||
LOG_FILE="/var/log/bible-chat-maintenance.log"
|
||||
COMPOSE_FILE="docker-compose.prod.yml"
|
||||
|
||||
# Function to log messages
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Function to run database maintenance
|
||||
run_db_maintenance() {
|
||||
echo -e "${YELLOW}📊 Running database maintenance...${NC}"
|
||||
|
||||
# Run optimization script
|
||||
docker-compose -f "$COMPOSE_FILE" exec -T postgres psql -U bible_admin -d bible_chat -f /docker-entrypoint-initdb.d/optimize-db.sql
|
||||
|
||||
# Run cleanup function
|
||||
docker-compose -f "$COMPOSE_FILE" exec -T postgres psql -U bible_admin -d bible_chat -c "SELECT cleanup_old_data();"
|
||||
|
||||
# Analyze performance
|
||||
docker-compose -f "$COMPOSE_FILE" exec -T postgres psql -U bible_admin -d bible_chat -c "SELECT analyze_query_performance();"
|
||||
|
||||
log "Database maintenance completed"
|
||||
}
|
||||
|
||||
# Function to create backup
|
||||
create_backup() {
|
||||
echo -e "${YELLOW}💾 Creating database backup...${NC}"
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Generate backup filename with timestamp
|
||||
BACKUP_FILE="$BACKUP_DIR/bible-chat-$(date +%Y%m%d_%H%M%S).sql"
|
||||
|
||||
# Create database backup
|
||||
docker-compose -f "$COMPOSE_FILE" exec -T postgres pg_dump -U bible_admin -d bible_chat > "$BACKUP_FILE"
|
||||
|
||||
# Compress backup
|
||||
gzip "$BACKUP_FILE"
|
||||
|
||||
# Remove backups older than 30 days
|
||||
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +30 -delete
|
||||
|
||||
log "Backup created: ${BACKUP_FILE}.gz"
|
||||
}
|
||||
|
||||
# Function to update containers
|
||||
update_containers() {
|
||||
echo -e "${YELLOW}🐳 Updating containers...${NC}"
|
||||
|
||||
# Pull latest images
|
||||
docker-compose -f "$COMPOSE_FILE" pull
|
||||
|
||||
# Restart services with zero downtime
|
||||
docker-compose -f "$COMPOSE_FILE" up -d --remove-orphans
|
||||
|
||||
# Remove unused images
|
||||
docker image prune -f
|
||||
|
||||
log "Containers updated"
|
||||
}
|
||||
|
||||
# Function to check disk space
|
||||
check_disk_space() {
|
||||
echo -e "${YELLOW}💽 Checking disk space...${NC}"
|
||||
|
||||
# Check available disk space (warn if less than 10% free)
|
||||
DISK_USAGE=$(df / | awk 'NR==2{printf "%.0f", $5}')
|
||||
|
||||
if [ "$DISK_USAGE" -gt 90 ]; then
|
||||
echo -e "${RED}⚠️ Warning: Disk usage is ${DISK_USAGE}%${NC}"
|
||||
log "WARNING: High disk usage - ${DISK_USAGE}%"
|
||||
else
|
||||
echo -e "${GREEN}✅ Disk usage is ${DISK_USAGE}%${NC}"
|
||||
log "Disk usage check passed - ${DISK_USAGE}%"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check service health
|
||||
check_service_health() {
|
||||
echo -e "${YELLOW}🏥 Checking service health...${NC}"
|
||||
|
||||
# Check if containers are running
|
||||
if docker-compose -f "$COMPOSE_FILE" ps | grep -q "Up"; then
|
||||
echo -e "${GREEN}✅ Services are running${NC}"
|
||||
|
||||
# Check application health endpoint
|
||||
if curl -f -s http://localhost/api/health > /dev/null; then
|
||||
echo -e "${GREEN}✅ Application health check passed${NC}"
|
||||
log "Health check passed"
|
||||
else
|
||||
echo -e "${RED}❌ Application health check failed${NC}"
|
||||
log "ERROR: Application health check failed"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}❌ Some services are not running${NC}"
|
||||
log "ERROR: Services not running properly"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to rotate logs
|
||||
rotate_logs() {
|
||||
echo -e "${YELLOW}📋 Rotating logs...${NC}"
|
||||
|
||||
# Rotate application logs
|
||||
docker-compose -f "$COMPOSE_FILE" exec app sh -c "find /app/.next -name '*.log' -size +100M -delete" 2>/dev/null || true
|
||||
|
||||
# Rotate docker logs
|
||||
docker-compose -f "$COMPOSE_FILE" logs --tail=1000 app > /var/log/bible-chat-app.log 2>/dev/null || true
|
||||
|
||||
# Compress old maintenance logs
|
||||
find /var/log -name "bible-chat-maintenance.log.*" -size +10M -exec gzip {} \; 2>/dev/null || true
|
||||
|
||||
log "Log rotation completed"
|
||||
}
|
||||
|
||||
# Function to generate performance report
|
||||
generate_performance_report() {
|
||||
echo -e "${YELLOW}📈 Generating performance report...${NC}"
|
||||
|
||||
REPORT_FILE="/var/log/bible-chat-performance-$(date +%Y%m%d).log"
|
||||
|
||||
{
|
||||
echo "=== Bible Chat Performance Report - $(date) ==="
|
||||
echo ""
|
||||
echo "=== Database Statistics ==="
|
||||
docker-compose -f "$COMPOSE_FILE" exec -T postgres psql -U bible_admin -d bible_chat -c "SELECT * FROM get_database_stats();"
|
||||
echo ""
|
||||
echo "=== Container Statistics ==="
|
||||
docker stats --no-stream
|
||||
echo ""
|
||||
echo "=== Disk Usage ==="
|
||||
df -h
|
||||
echo ""
|
||||
} > "$REPORT_FILE"
|
||||
|
||||
log "Performance report generated: $REPORT_FILE"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log "Maintenance started"
|
||||
|
||||
# Parse command line arguments
|
||||
case "${1:-all}" in
|
||||
"backup")
|
||||
create_backup
|
||||
;;
|
||||
"cleanup")
|
||||
run_db_maintenance
|
||||
rotate_logs
|
||||
;;
|
||||
"update")
|
||||
update_containers
|
||||
;;
|
||||
"health")
|
||||
check_service_health
|
||||
;;
|
||||
"report")
|
||||
generate_performance_report
|
||||
;;
|
||||
"all")
|
||||
check_disk_space
|
||||
check_service_health
|
||||
create_backup
|
||||
run_db_maintenance
|
||||
rotate_logs
|
||||
generate_performance_report
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {backup|cleanup|update|health|report|all}"
|
||||
echo ""
|
||||
echo " backup - Create database backup"
|
||||
echo " cleanup - Run database maintenance and log rotation"
|
||||
echo " update - Update and restart containers"
|
||||
echo " health - Check service health"
|
||||
echo " report - Generate performance report"
|
||||
echo " all - Run all maintenance tasks (default)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "${GREEN}✅ Maintenance completed successfully!${NC}"
|
||||
log "Maintenance completed successfully"
|
||||
}
|
||||
|
||||
# Ensure script is run as root or with sudo
|
||||
if [[ $EUID -ne 0 ]] && [[ -z "$SUDO_USER" ]]; then
|
||||
echo -e "${RED}This script must be run as root or with sudo${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
140
scripts/optimize-db.sql
Normal file
140
scripts/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;
|
||||
Reference in New Issue
Block a user