|
import { serve } from "bun"; |
|
import { readFile } from "fs/promises"; |
|
import path from "path"; |
|
|
|
|
|
interface Item { |
|
name?: string; |
|
full_name?: string; |
|
owner?: string; |
|
description?: string; |
|
topics?: string[]; |
|
created_at?: string; |
|
usage_count?: number; |
|
} |
|
|
|
|
|
|
|
const DB_PATH = "../database/database"; |
|
const PORT = 7860; |
|
const RELOAD_INTERVAL_MS = 3600000; |
|
|
|
const corsHeaders = { |
|
"Content-Type": "application/json", |
|
"Access-Control-Allow-Origin": "*", |
|
}; |
|
|
|
|
|
|
|
let packages: Item[] = []; |
|
let programs: Item[] = []; |
|
let sorted: { |
|
packages: { latest: Item[]; mostUsed: Item[] }; |
|
programs: { latest: Item[]; mostUsed: Item[] }; |
|
} = { packages: { latest: [], mostUsed: [] }, programs: { latest: [], mostUsed: [] } }; |
|
|
|
|
|
|
|
function getSortedData(packages: Item[], programs: Item[]) { |
|
return { |
|
packages: { |
|
latest: [...packages].sort(sortByDate), |
|
mostUsed: [...packages].sort(sortByUsage), |
|
}, |
|
programs: { |
|
latest: [...programs].sort(sortByDate), |
|
mostUsed: [...programs].sort(sortByUsage), |
|
}, |
|
}; |
|
} |
|
|
|
function sortByDate(a: Item, b: Item): number { |
|
return new Date(b.created_at || "").getTime() - new Date(a.created_at || "").getTime(); |
|
} |
|
|
|
function sortByUsage(a: Item, b: Item): number { |
|
return (b.usage_count || 0) - (a.usage_count || 0); |
|
} |
|
|
|
|
|
|
|
|
|
function filterItems(items: Item[], query: string | null, topic: string | null): Item[] { |
|
return items.filter(({ name, full_name, description, topics }) => { |
|
if (topic && !topics?.some(t => t.toLowerCase() === topic)) return false; |
|
if (!query) return true; |
|
const lowerQ = query.toLowerCase(); |
|
return [name, full_name, description, ...(topics || [])] |
|
.some(field => field?.toLowerCase().includes(lowerQ)); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
function getPaginated(items: Item[], page: number = 0, size: number = 10): Item[] { |
|
const start = page * size; |
|
return items.slice(start, start + size); |
|
} |
|
|
|
|
|
|
|
|
|
function findItem(items: Item[], owner: string, repo: string): Item | undefined { |
|
return items.find(({ owner: o, name, full_name }) => |
|
(o?.toLowerCase() === owner && name?.toLowerCase() === repo) || |
|
full_name?.toLowerCase() === `${owner}/${repo}` |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
function parseRange(str: string | null, max: number): [number, number] { |
|
const match = str?.match(/^(\d+)\.\.(\d+)$/); |
|
const [start, end] = match |
|
? [parseInt(match[1]), parseInt(match[2])] |
|
: [0, 10]; |
|
return [Math.max(0, start), Math.min(max, end)]; |
|
} |
|
|
|
|
|
|
|
|
|
async function reloadData() { |
|
try { |
|
const pkgPath = path.resolve(__dirname, `${DB_PATH}/packages.json`); |
|
const prgPath = path.resolve(__dirname, `${DB_PATH}/programs.json`); |
|
const [pkgData, prgData] = await Promise.all([ |
|
readFile(pkgPath, "utf8"), |
|
readFile(prgPath, "utf8"), |
|
]); |
|
packages = JSON.parse(pkgData); |
|
programs = JSON.parse(prgData); |
|
sorted = getSortedData(packages, programs); |
|
console.log(`[${new Date().toISOString()}] Database reloaded`); |
|
} catch (err) { |
|
console.error("Error reloading database files:", err); |
|
} |
|
} |
|
|
|
|
|
reloadData(); |
|
setInterval(reloadData, RELOAD_INTERVAL_MS); |
|
|
|
|
|
|
|
serve({ |
|
port: PORT, |
|
fetch(req: Request): Response | Promise<Response> { |
|
const url = new URL(req.url); |
|
const { pathname, searchParams } = url; |
|
const q = searchParams.get("q")?.trim().toLowerCase() ?? null; |
|
const filter = searchParams.get("filter")?.trim().toLowerCase() ?? null; |
|
|
|
|
|
if (req.method === "OPTIONS") { |
|
return new Response(null, { |
|
status: 204, |
|
headers: { |
|
...corsHeaders, |
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS", |
|
"Access-Control-Allow-Headers": "Content-Type", |
|
}, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
if (pathname === "/api/searchPackages") { |
|
const result = filterItems(packages, q, filter).slice(0, 25); |
|
return Response.json(result, { headers: corsHeaders }); |
|
} |
|
if (pathname === "/api/searchPrograms") { |
|
const result = filterItems(programs, q, filter).slice(0, 25); |
|
return Response.json(result, { headers: corsHeaders }); |
|
} |
|
|
|
|
|
if (pathname === "/api/infiniteScrollPackages") { |
|
const page = parseInt(searchParams.get("pageNumber") || "0", 10); |
|
if (isNaN(page) || page < 0) |
|
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders }); |
|
return Response.json(getPaginated(packages, page), { headers: corsHeaders }); |
|
} |
|
if (pathname === "/api/infiniteScrollPrograms") { |
|
const page = parseInt(searchParams.get("pageNumber") || "0", 10); |
|
if (isNaN(page) || page < 0) |
|
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders }); |
|
return Response.json(getPaginated(programs, page), { headers: corsHeaders }); |
|
} |
|
|
|
|
|
const programMatch = pathname.match(/^\/api\/programs\/([^\/]+)\/([^\/]+)$/); |
|
if (programMatch) { |
|
const [_, owner, repo] = programMatch.map(s => s.toLowerCase()); |
|
const found = findItem(programs, owner, repo); |
|
return Response.json(found || { error: "Program not found" }, { |
|
status: found ? 200 : 404, |
|
headers: corsHeaders, |
|
}); |
|
} |
|
const packageMatch = pathname.match(/^\/api\/packages\/([^\/]+)\/([^\/]+)$/); |
|
if (packageMatch) { |
|
const [_, owner, repo] = packageMatch.map(s => s.toLowerCase()); |
|
const found = findItem(packages, owner, repo); |
|
return Response.json(found || { error: "Package not found" }, { |
|
status: found ? 200 : 404, |
|
headers: corsHeaders, |
|
}); |
|
} |
|
|
|
|
|
if (pathname === "/api/indexDetailsPackages") { |
|
const section = searchParams.get("section"); |
|
if (section !== "latestRepos" && section !== "mostUsed") { |
|
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders }); |
|
} |
|
const sortKey = section === "latestRepos" ? "latest" : "mostUsed"; |
|
const data = sorted.packages[sortKey as keyof typeof sorted.packages] ?? packages; |
|
const [start, end] = parseRange(searchParams.get("range"), data.length); |
|
return Response.json(data.slice(start, end), { headers: corsHeaders }); |
|
} |
|
if (pathname === "/api/indexDetailsPrograms") { |
|
const section = searchParams.get("section"); |
|
if (section !== "latestRepos" && section !== "mostUsed") { |
|
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders }); |
|
} |
|
const sortKey = section === "latestRepos" ? "latest" : "mostUsed"; |
|
const data = sorted.programs[sortKey as keyof typeof sorted.programs] ?? programs; |
|
const [start, end] = parseRange(searchParams.get("range"), data.length); |
|
return Response.json(data.slice(start, end), { headers: corsHeaders }); |
|
} |
|
|
|
|
|
return new Response("Not Found", { |
|
status: 404, |
|
headers: corsHeaders, |
|
}); |
|
}, |
|
}); |
|
|
|
console.log(`Server running on http://localhost:${PORT}`); |