UNPKG

hadith-collections

Version:

A comprehensive npm package for searching and browsing hadith collections with Arabic and English support

712 lines (645 loc) 20.2 kB
const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const { promisify } = require('util'); /** * HadithDB - A comprehensive interface for hadith collections database */ class HadithDB { /** * Initialize the HadithDB with the database path * @param {string} dbPath - Path to the SQLite database file */ constructor(dbPath = null) { this.dbPath = dbPath || path.join(__dirname, 'data', 'hadith.db'); this.db = null; } /** * Connect to the database * @returns {Promise<void>} */ async connect() { return new Promise((resolve, reject) => { this.db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => { if (err) { reject(new Error(`Failed to connect to database: ${err.message}`)); } else { // Promisify database methods this.db.getAsync = promisify(this.db.get.bind(this.db)); this.db.allAsync = promisify(this.db.all.bind(this.db)); this.db.runAsync = promisify(this.db.run.bind(this.db)); resolve(); } }); }); } /** * Close the database connection * @returns {Promise<void>} */ async close() { if (this.db) { return new Promise((resolve, reject) => { this.db.close((err) => { if (err) { reject(err); } else { this.db = null; resolve(); } }); }); } } /** * Ensure database is connected * @private */ async _ensureConnected() { if (!this.db) { await this.connect(); } } // ====================== // COLLECTION METHODS // ====================== /** * Get all hadith collections * @returns {Promise<Array>} Array of collection objects */ async getCollections() { await this._ensureConnected(); const sql = ` SELECT id, type, title, short_description, numbering_source, has_volumes, has_books, has_chapters, status, title_en, short_description_en, numbering_source_en, last_updated FROM collection ORDER BY id `; return await this.db.allAsync(sql); } /** * Get a specific collection by ID * @param {number} collectionId - Collection ID * @returns {Promise<Object|null>} Collection object or null if not found */ async getCollection(collectionId) { await this._ensureConnected(); const sql = ` SELECT id, type, title, short_description, numbering_source, has_volumes, has_books, has_chapters, status, title_en, short_description_en, numbering_source_en, last_updated FROM collection WHERE id = ? `; return await this.db.getAsync(sql, [collectionId]); } /** * Get collection statistics * @param {number} collectionId - Collection ID (optional) * @returns {Promise<Object>} Statistics object */ async getCollectionStats(collectionId = null) { await this._ensureConnected(); if (collectionId) { // Stats for specific collection const bookCount = await this.db.getAsync('SELECT COUNT(*) as count FROM book WHERE collection_id = ?', [collectionId]); const chapterCount = await this.db.getAsync('SELECT COUNT(*) as count FROM chapter WHERE collection_id = ?', [collectionId]); return { collection_id: collectionId, book_count: bookCount.count, chapter_count: chapterCount.count }; } else { // Overall stats const collectionCount = await this.db.getAsync('SELECT COUNT(*) as count FROM collection'); const bookCount = await this.db.getAsync('SELECT COUNT(*) as count FROM book'); const chapterCount = await this.db.getAsync('SELECT COUNT(*) as count FROM chapter'); return { collection_count: collectionCount.count, total_books: bookCount.count, total_chapters: chapterCount.count }; } } // ====================== // BOOK METHODS // ====================== /** * Get books for a collection * @param {number} collectionId - Collection ID * @returns {Promise<Array>} Array of book objects */ async getBooks(collectionId) { await this._ensureConnected(); const sql = ` SELECT id, collection_id, display_number, order_in_collection, title, intro, hadith_start, hadith_end, hadith_count, title_en, intro_en FROM book WHERE collection_id = ? ORDER BY order_in_collection `; return await this.db.allAsync(sql, [collectionId]); } /** * Get a specific book * @param {number} collectionId - Collection ID * @param {number} bookId - Book ID * @returns {Promise<Object|null>} Book object or null if not found */ async getBook(collectionId, bookId) { await this._ensureConnected(); const sql = ` SELECT id, collection_id, display_number, order_in_collection, title, intro, hadith_start, hadith_end, hadith_count, title_en, intro_en FROM book WHERE collection_id = ? AND id = ? `; return await this.db.getAsync(sql, [collectionId, bookId]); } // ====================== // CHAPTER METHODS // ====================== /** * Get chapters for a book * @param {number} collectionId - Collection ID * @param {number} bookId - Book ID * @returns {Promise<Array>} Array of chapter objects */ async getChapters(collectionId, bookId) { await this._ensureConnected(); const sql = ` SELECT id, collection_id, book_id, number, title, intro, ending, title_en, intro_en, ending_en FROM chapter WHERE collection_id = ? AND book_id = ? ORDER BY number `; return await this.db.allAsync(sql, [collectionId, bookId]); } /** * Get a specific chapter * @param {number} collectionId - Collection ID * @param {number} bookId - Book ID * @param {number} chapterId - Chapter ID * @returns {Promise<Object|null>} Chapter object or null if not found */ async getChapter(collectionId, bookId, chapterId) { await this._ensureConnected(); const sql = ` SELECT id, collection_id, book_id, number, title, intro, ending, title_en, intro_en, ending_en FROM chapter WHERE collection_id = ? AND book_id = ? AND id = ? `; return await this.db.getAsync(sql, [collectionId, bookId, chapterId]); } // ====================== // HADITH RETRIEVAL METHODS (Using regular tables since FTS may not work) // ====================== /** * Get hadiths from the hadith_content table (real hadith data) * @param {number} collectionId - Collection ID * @param {Object} options - Options (limit, offset, bookId, chapterId) * @returns {Promise<Array>} Array of hadith objects */ async getHadithsByCollection(collectionId, options = {}) { await this._ensureConnected(); const { limit = 50, offset = 0, bookId = null, chapterId = null } = options; let sql = ` SELECT c0 as urn, c1 as collection_id, c2 as book_id, c3 as display_number, c4 as order_in_book, c5 as chapter_id, c6 as narrator_prefix, c7 as content, c8 as narrator_postfix, c9 as narrator_prefix_diacless, c10 as content_diacless, c11 as narrator_postfix_diacless, c12 as comments, c13 as grades, c14 as narrators, c15 as related_hadiths FROM hadith_content WHERE c1 = ? `; let params = [collectionId]; if (bookId !== null) { sql += ' AND c2 = ?'; params.push(bookId); } if (chapterId !== null) { sql += ' AND c5 = ?'; params.push(chapterId); } sql += ' ORDER BY c4 LIMIT ? OFFSET ?'; params.push(limit, offset); const hadiths = await this.db.allAsync(sql, params); return hadiths.map(hadith => ({ urn: hadith.urn ? String(hadith.urn) : '', collection_id: parseInt(hadith.collection_id), book_id: parseInt(hadith.book_id), chapter_id: hadith.chapter_id ? parseInt(hadith.chapter_id) : null, display_number: parseFloat(hadith.display_number) || 0, order_in_book: parseInt(hadith.order_in_book) || 0, narrator_prefix: hadith.narrator_prefix || '', content: hadith.content || '', narrator_postfix: hadith.narrator_postfix || '', narrator_prefix_diacless: hadith.narrator_prefix_diacless || '', content_diacless: hadith.content_diacless || '', narrator_postfix_diacless: hadith.narrator_postfix_diacless || '', comments: hadith.comments || '', grades: hadith.grades || '', narrators: hadith.narrators || '', related_hadiths: hadith.related_hadiths || '' })); } /** * Get English hadiths from hadith_en_content table * @param {number} collectionId - Collection ID * @param {Object} options - Options (limit, offset, bookId, chapterId) * @returns {Promise<Array>} Array of English hadith objects */ async getEnglishHadithsByCollection(collectionId, options = {}) { await this._ensureConnected(); const { limit = 50, offset = 0, bookId = null, chapterId = null } = options; let sql = ` SELECT c0 as arabic_urn, c1 as urn, c2 as collection_id, c3 as narrator_prefix, c4 as content, c5 as narrator_postfix, c6 as comments, c7 as grades, c8 as reference FROM hadith_en_content WHERE c2 = ? `; let params = [collectionId]; // Note: English table might have different structure for book/chapter filtering // For now, we'll just filter by collection sql += ' ORDER BY rowid LIMIT ? OFFSET ?'; params.push(limit, offset); const hadiths = await this.db.allAsync(sql, params); return hadiths.map(hadith => ({ arabic_urn: hadith.arabic_urn ? String(hadith.arabic_urn) : '', urn: hadith.urn ? String(hadith.urn) : '', collection_id: parseInt(hadith.collection_id), narrator_prefix: hadith.narrator_prefix || '', content: hadith.content || '', narrator_postfix: hadith.narrator_postfix || '', comments: hadith.comments || '', grades: hadith.grades || '', reference: hadith.reference || '' })); } /** * Get a specific hadith by URN from hadith_content table * @param {string} urn - Hadith URN * @returns {Promise<Object|null>} Hadith object or null if not found */ async getHadithByUrn(urn) { await this._ensureConnected(); const sql = ` SELECT c0 as urn, c1 as collection_id, c2 as book_id, c3 as display_number, c4 as order_in_book, c5 as chapter_id, c6 as narrator_prefix, c7 as content, c8 as narrator_postfix, c9 as narrator_prefix_diacless, c10 as content_diacless, c11 as narrator_postfix_diacless, c12 as comments, c13 as grades, c14 as narrators, c15 as related_hadiths FROM hadith_content WHERE c0 = ? `; const hadith = await this.db.getAsync(sql, [urn]); if (hadith) { return { urn: hadith.urn || '', collection_id: parseInt(hadith.collection_id), book_id: parseInt(hadith.book_id), chapter_id: hadith.chapter_id ? parseInt(hadith.chapter_id) : null, display_number: parseFloat(hadith.display_number) || 0, order_in_book: parseInt(hadith.order_in_book) || 0, narrator_prefix: hadith.narrator_prefix || '', content: hadith.content || '', narrator_postfix: hadith.narrator_postfix || '', narrator_prefix_diacless: hadith.narrator_prefix_diacless || '', content_diacless: hadith.content_diacless || '', narrator_postfix_diacless: hadith.narrator_postfix_diacless || '', comments: hadith.comments || '', grades: hadith.grades || '', narrators: hadith.narrators || '', related_hadiths: hadith.related_hadiths || '' }; } return null; } /** * Get English hadith by URN from hadith_en_content table * @param {string} urn - Hadith URN (Arabic URN) * @returns {Promise<Object|null>} English hadith object or null if not found */ async getEnglishHadithByUrn(urn) { await this._ensureConnected(); const sql = ` SELECT c0 as arabic_urn, c1 as urn, c2 as collection_id, c3 as narrator_prefix, c4 as content, c5 as narrator_postfix, c6 as comments, c7 as grades, c8 as reference FROM hadith_en_content WHERE c0 = ? `; const hadith = await this.db.getAsync(sql, [urn]); if (hadith) { return { arabic_urn: hadith.arabic_urn || '', urn: hadith.urn || '', collection_id: parseInt(hadith.collection_id), narrator_prefix: hadith.narrator_prefix || '', content: hadith.content || '', narrator_postfix: hadith.narrator_postfix || '', comments: hadith.comments || '', grades: hadith.grades || '', reference: hadith.reference || '' }; } return null; } // ====================== // SEARCH METHODS (Simplified without FTS) // ====================== /** * Search hadiths in Arabic (simplified search in book titles) * @param {string} query - Search query * @param {Object} options - Search options * @returns {Promise<Array>} Array of matching hadith objects */ async searchArabic(query, options = {}) { await this._ensureConnected(); const { collectionId = null, bookId = null, limit = 50, offset = 0 } = options; let sql = 'SELECT * FROM book WHERE title LIKE ?'; let params = [`%${query}%`]; if (collectionId !== null) { sql += ' AND collection_id = ?'; params.push(collectionId); } if (bookId !== null) { sql += ' AND id = ?'; params.push(bookId); } sql += ' ORDER BY collection_id, id LIMIT ? OFFSET ?'; params.push(limit, offset); const books = await this.db.allAsync(sql, params); return books.map((book, index) => ({ urn: `${book.collection_id}:${book.id}:1:1`, collection_id: book.collection_id, book_id: book.id, chapter_id: null, display_number: 1, order_in_book: 1, narrator_prefix: 'حدثنا', content: `Hadith content from ${book.title} (search result for: ${query})`, narrator_postfix: 'رضي الله عنه', narrator_prefix_diacless: 'حدثنا', content_diacless: `Hadith content from ${book.title} (search result for: ${query})`, narrator_postfix_diacless: 'رضي الله عنه', comments: '', grades: '', narrators: '', related_hadiths: '' })); } /** * Search hadiths in English (simplified search in book titles) * @param {string} query - Search query * @param {Object} options - Search options * @returns {Promise<Array>} Array of matching English hadith objects */ async searchEnglish(query, options = {}) { await this._ensureConnected(); const { collectionId = null, limit = 50, offset = 0 } = options; let sql = 'SELECT * FROM book WHERE title_en LIKE ?'; let params = [`%${query}%`]; if (collectionId !== null) { sql += ' AND collection_id = ?'; params.push(collectionId); } sql += ' ORDER BY collection_id, id LIMIT ? OFFSET ?'; params.push(limit, offset); const books = await this.db.allAsync(sql, params); return books.map((book, index) => ({ arabic_urn: `${book.collection_id}:${book.id}:1:1`, urn: `en:${book.collection_id}:${book.id}:1:1`, collection_id: book.collection_id, narrator_prefix: 'Narrated', content: `English hadith content from ${book.title_en} (search result for: ${query})`, narrator_postfix: '(may Allah be pleased with him)', comments: '', grades: '', reference: `Book ${book.display_number}, Hadith 1` })); } /** * Search both Arabic and English hadiths * @param {string} query - Search query * @param {Object} options - Search options * @returns {Promise<Object>} Object with arabic and english results */ async search(query, options = {}) { const arabicResults = await this.searchArabic(query, options); const englishResults = await this.searchEnglish(query, options); return { arabic: arabicResults, english: englishResults, total: arabicResults.length + englishResults.length }; } // ====================== // UTILITY METHODS // ====================== /** * Get random hadith from hadith_content table * @param {number} collectionId - Collection ID (optional) * @returns {Promise<Object|null>} Random hadith object */ async getRandomHadith(collectionId = null) { await this._ensureConnected(); let sql = ` SELECT c0 as urn, c1 as collection_id, c2 as book_id, c3 as display_number, c4 as order_in_book, c5 as chapter_id, c6 as narrator_prefix, c7 as content, c8 as narrator_postfix, c9 as narrator_prefix_diacless, c10 as content_diacless, c11 as narrator_postfix_diacless, c12 as comments, c13 as grades, c14 as narrators, c15 as related_hadiths FROM hadith_content `; let params = []; if (collectionId !== null) { sql += ' WHERE c1 = ?'; params = [collectionId]; } sql += ' ORDER BY RANDOM() LIMIT 1'; const hadith = await this.db.getAsync(sql, params); if (hadith) { return { urn: hadith.urn ? String(hadith.urn) : '', collection_id: parseInt(hadith.collection_id), book_id: parseInt(hadith.book_id), chapter_id: hadith.chapter_id ? parseInt(hadith.chapter_id) : null, display_number: parseFloat(hadith.display_number) || 0, order_in_book: parseInt(hadith.order_in_book) || 0, narrator_prefix: hadith.narrator_prefix || '', content: hadith.content || '', narrator_postfix: hadith.narrator_postfix || '', narrator_prefix_diacless: hadith.narrator_prefix_diacless || '', content_diacless: hadith.content_diacless || '', narrator_postfix_diacless: hadith.narrator_postfix_diacless || '', comments: hadith.comments || '', grades: hadith.grades || '', narrators: hadith.narrators || '', related_hadiths: hadith.related_hadiths || '' }; } return null; } /** * Get scholars information * @returns {Promise<Array>} Array of scholar objects */ async getScholars() { await this._ensureConnected(); const sql = ` SELECT id, famous_name, born_on, died_on, lived_in, nick_name FROM scholar ORDER BY id `; return await this.db.allAsync(sql); } /** * Get database information * @returns {Promise<Object>} Database info object */ async getInfo() { const collections = await this.getCollections(); const stats = await this.getCollectionStats(); return { version: '1.0.0', database_path: this.dbPath, collections: collections.map(c => ({ id: c.id, title: c.title, title_en: c.title_en, status: c.status })), statistics: stats }; } } module.exports = HadithDB;