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
JavaScript
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;