a new start
This commit is contained in:
330
lib/db.ts
Normal file
330
lib/db.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { join } from 'path'
|
||||
|
||||
// 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) {
|
||||
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<T> {
|
||||
results: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-text search using FTS5
|
||||
*/
|
||||
export function searchRepositories(options: SearchOptions): PaginatedResults<SearchResult> {
|
||||
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, '<mark>', '</mark>', '...', 32) as snippet
|
||||
FROM readmes_fts fts
|
||||
JOIN repositories r ON fts.rowid = 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<Repository> {
|
||||
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[]
|
||||
}
|
||||
Reference in New Issue
Block a user