import Database from 'better-sqlite3' import { join } from 'path' import { existsSync } from 'fs' // Database path - using user's .awesome directory const DB_PATH = process.env.AWESOME_DB_PATH || join(process.env.HOME || '', '.awesome', 'awesome.db') let db: Database.Database | null = null export function getDb(): Database.Database { if (!db) { // Check if database file exists before trying to open it if (!existsSync(DB_PATH)) { throw new Error(`Database file not found at ${DB_PATH}`) } db = new Database(DB_PATH, { readonly: true }) // Enable WAL mode for better concurrency db.pragma('journal_mode = WAL') } return db } export interface AwesomeList { id: number name: string url: string description: string | null category: string | null stars: number | null forks: number | null last_commit: string | null level: number | null parent_id: number | null added_at: string | null last_updated: string | null } export interface Repository { id: number awesome_list_id: number name: string url: string description: string | null stars: number | null forks: number | null watchers: number | null language: string | null topics: string | null last_commit: string | null created_at: string | null added_at: string | null } export interface Readme { id: number repository_id: number content: string | null raw_content: string | null version_hash: string | null indexed_at: string | null } export interface SearchResult { repository_id: number repository_name: string repository_url: string description: string | null stars: number | null language: string | null topics: string | null awesome_list_name: string | null awesome_list_category: string | null rank: number snippet: string | null } export interface SearchOptions { query: string limit?: number offset?: number language?: string minStars?: number category?: string sortBy?: 'relevance' | 'stars' | 'recent' } export interface PaginatedResults { results: T[] total: number page: number pageSize: number totalPages: number } /** * Full-text search using FTS5 */ export function searchRepositories(options: SearchOptions): PaginatedResults { const db = getDb() const { query, limit = 20, offset = 0, language, minStars, category, sortBy = 'relevance' } = options // Build FTS query const ftsQuery = query .trim() .split(/\s+/) .map(term => `"${term}"*`) .join(' OR ') let sql = ` SELECT r.id as repository_id, r.name as repository_name, r.url as repository_url, r.description, r.stars, r.language, r.topics, al.name as awesome_list_name, al.category as awesome_list_category, fts.rank, snippet(readmes_fts, 2, '', '', '...', 32) as snippet FROM readmes_fts fts JOIN readmes rm ON fts.rowid = rm.rowid JOIN repositories r ON rm.repository_id = r.id LEFT JOIN awesome_lists al ON r.awesome_list_id = al.id WHERE readmes_fts MATCH ? ` const params: any[] = [ftsQuery] // Add filters if (language) { sql += ` AND r.language = ?` params.push(language) } if (minStars !== undefined) { sql += ` AND r.stars >= ?` params.push(minStars) } if (category) { sql += ` AND al.category = ?` params.push(category) } // Add sorting switch (sortBy) { case 'stars': sql += ` ORDER BY r.stars DESC, fts.rank` break case 'recent': sql += ` ORDER BY r.last_commit DESC, fts.rank` break default: sql += ` ORDER BY fts.rank` } // Count total results (use [\s\S]*? for multiline matching) const countSql = sql.replace(/SELECT[\s\S]*?FROM/, 'SELECT COUNT(*) as total FROM') .replace(/ORDER BY[\s\S]*$/, '') const totalResult = db.prepare(countSql).get(...params) as { total: number } const total = totalResult.total // Add pagination sql += ` LIMIT ? OFFSET ?` params.push(limit, offset) const results = db.prepare(sql).all(...params) as SearchResult[] const page = Math.floor(offset / limit) + 1 const totalPages = Math.ceil(total / limit) return { results, total, page, pageSize: limit, totalPages } } /** * Get all awesome lists with optional category filter */ export function getAwesomeLists(category?: string): AwesomeList[] { const db = getDb() let sql = ` SELECT * FROM awesome_lists WHERE 1=1 ` const params: any[] = [] if (category) { sql += ` AND category = ?` params.push(category) } sql += ` ORDER BY stars DESC, name ASC` return db.prepare(sql).all(...params) as AwesomeList[] } /** * Get repositories by awesome list ID */ export function getRepositoriesByList(listId: number, limit = 50, offset = 0): PaginatedResults { const db = getDb() const countSql = `SELECT COUNT(*) as total FROM repositories WHERE awesome_list_id = ?` const totalResult = db.prepare(countSql).get(listId) as { total: number } const total = totalResult.total const sql = ` SELECT * FROM repositories WHERE awesome_list_id = ? ORDER BY stars DESC, name ASC LIMIT ? OFFSET ? ` const results = db.prepare(sql).all(listId, limit, offset) as Repository[] const page = Math.floor(offset / limit) + 1 const totalPages = Math.ceil(total / limit) return { results, total, page, pageSize: limit, totalPages } } /** * Get repository by ID with README content */ export function getRepositoryWithReadme(repositoryId: number): (Repository & { readme: Readme | null }) | null { const db = getDb() const repo = db.prepare(` SELECT * FROM repositories WHERE id = ? `).get(repositoryId) as Repository | undefined if (!repo) { return null } const readme = db.prepare(` SELECT raw_content as content FROM readmes WHERE repository_id = ? `).get(repositoryId) as Readme | undefined return { ...repo, readme: readme || null } } /** * Get all unique categories */ export function getCategories(): { name: string; count: number }[] { const db = getDb() return db.prepare(` SELECT category as name, COUNT(*) as count FROM awesome_lists WHERE category IS NOT NULL GROUP BY category ORDER BY count DESC, category ASC `).all() as { name: string; count: number }[] } /** * Get all unique languages */ export function getLanguages(): { name: string; count: number }[] { const db = getDb() return db.prepare(` SELECT language as name, COUNT(*) as count FROM repositories WHERE language IS NOT NULL GROUP BY language ORDER BY count DESC, language ASC LIMIT 50 `).all() as { name: string; count: number }[] } /** * Get database statistics */ export function getStats() { const db = getDb() const stats = { lists: db.prepare('SELECT COUNT(*) as count FROM awesome_lists').get() as { count: number }, repositories: db.prepare('SELECT COUNT(*) as count FROM repositories').get() as { count: number }, readmes: db.prepare('SELECT COUNT(*) as count FROM readmes').get() as { count: number }, lastUpdated: db.prepare('SELECT MAX(last_updated) as date FROM awesome_lists').get() as { date: string | null } } return { totalLists: stats.lists.count, totalRepositories: stats.repositories.count, totalReadmes: stats.readmes.count, lastUpdated: stats.lastUpdated.date } } /** * Get trending repositories (most stars) */ export function getTrendingRepositories(limit = 10): Repository[] { const db = getDb() return db.prepare(` SELECT * FROM repositories WHERE stars IS NOT NULL ORDER BY stars DESC LIMIT ? `).all(limit) as Repository[] }